Руководство по Программированию Гомункулов

Тема в разделе "Торговцы и Кузнецы", создана пользователем maas, 27 май 2016.

  1. maas

    maas Игрок

    Гайд не мой, но я с трудом его нашел в кеше яндекса) все места где он был опубликован ранее уже канули в Лету. Ссылки оставил из уважения к его автору, хоть они уже и не работают :)
    Руководство по Программированию Гомункулов

    ~by Sanctus (http://www.asgardro.com/forum/member.php?u=110) of AsgardRO (http://www.asgardro.com)~


    Последнее обновление: 18.06.2009 -> Автор в армии до июня 2010


    Содержание:

    I: От Автора
    II: Ссылки
    III: Введение
    IV: Lua и азы программирования
    V: Редактор исходников SciTE
    VI: Функции и команды в AI
    VII: Разбор скрипта AI
    VIII: Полная хронология обработки скрипта
    IX: Помощь в начинаниях
    X: Примечание

    ---

    I. От Автора

    Данный гайд фактически является продолжением моего прошлого. В нем я постораюсь ознакомить читателей со следующей ступенью пользования гомункулами, а именно- их программированием.
    Надеюсь, что это руководство станет хорошим пособием для всех тех, кто решил ступить на трудный путь понимания устройства интеллекта гомункула, а также сможет продвинуть написание скриптов в массы!

    ---

    II. Ссылки


    Мануал языка Lua (http://www.lua.org/manual/5.1/) (Русская версия (http://www.lua.ru/doc/))
    Онлайн книга по Lua 5.0 (http://www.lua.org/pil/)
    Текстовый редактор SciTE (http://users.hfx.eastlink.ca/~gisdev/scite-1.70-setup-1.exe)
    Гайд по программированию AI от BlankZero (http://forums.roempire.com/showthread.php?t=118561)

    ---

    III. Введение

    Любая программа, начиная скриптами гомункула, кончая Windows- это нагромождения нулей и единиц, которые понимает и с которыми может работать только компьютер. Человек не в состоянии говорить с компьютером на его родном языке, поэтому люди придумали промежуточные языки, которые были бы ближе к человеку и его образу мышления. Однако любой язык, отличный от нулей и единиц, для компьютера будет пустым местом, поэтому к каждому языку программирования прилагается компилятор(Compiler) или интерпретатор(Interpreter)(применительно к Lua правильнее второе), которые делают из кода промежуточного языка машинный код(нули и единицы). Собственно, таким же образом скрипт гомункула, написанный на языке Lua, обрабатывается интерпретатором в вашем клиенте каждый раз при респавне(см. ГОМУНКУЛЫ :: Полное Руководство -> Общие сведения -> AI) гомункула, чтобы получить готовую программу(понятную клиенту) для управления вашим питомцем.
    Таким образом, программа, помимо своего рабочего состояния в машинном коде, также имеет текстовое состояние, которое называется исходником(Source) или скриптом(Script)(применительно к Lua правильнее второе). Исходники состоят из строк кода, каждая из которых, производит какие-то действия над данными, отдает команды для исполнения другим частям программы или другим программам. Как пример взаимодействия команд и работы над данными, можно назвать вычисление количества клеток до цели по координатам гомункула и цели, которые мы запросили у клиента(который до этого запросил их у сервера) через определенную команду. Далее опять может идти работа с данными, например, сравнивание результата с вычислениями расстояния до других целей, чтобы найти самую близкую.
    Всевозможные преобразования в языках программирования реализованы через функции. Как правило, к языку прилогаются базовые функции, которые по-умолчанию в него встроены(например функции сложения/умножения/деления чисел). К языку могут прилогаться и другие наборы функций, которые можно подключать к исходникам(чтобы получить доступ к этим функциям), их называют библиотеками(Library) или пакетами(Package)(например в Lua имеются библиотеки для работы с текстовыми сообщениями, для запращивания времени и даты, а также для многих других интересных операций). Те библиотеки, которые предоставляют нам разработчики языка, как правило, называют стандартными, они фактически являются частью языка, но разработчику программы они могут и не понадобится, тогда он их просто не подключает, чтобы с сэкономить в размерах программы и ее быстродействии. Однако стандартными библиотеками дело не ограничевается, так как любой пользователь может с помощью возможностей языка создавать свои функции(и также объединять их в библиотеки) и давать другим для подключения и пользования. Таким же образом, и вы сможете использовать встроенные функции, которые дали нам разработчики гомункулов, а также любые другие, которые захотите создать сами. Подробнее обо всем об этом я расскажу в следующих главах, а на этом, пожалуй, введение закончим.

    ---

    IV. Lua и азы программирования

    В целом, Lua очень простой и понятный язык программирования, по сравнению с такими "гигантами" как C и Java. Но, несмотря на это, он отвечает всем основным требованиям современного языка, совмещая в себе функциональность и простоту. Благодаря этому, его можно использовать во многих областях, независемо от того начинающий ли вы или уже весьма продвинутый программист.
    Реализован Lua на основе C, поэтому считается весьма быстрым языком среди скриптовых. В Lua нету основной функции, которая вызывается первой, и с которой начинается программа(как например функция main в C), так как Lua задумывался для работы поверх других программ, тоесть он предоставляет свои услуги, а программа сама решает как и какие функции использовать(например, какую запускать первой). Таким же образом и клиент Рагнарока сам ищет функцию AI в вашем скрипте и затем постоянно вызывает ее, пока ваш гомункул активен.
    В данном гайде я вскользь пробегусь по базовым элементам Lua. Большинство из них являются стандартными элементами любого языка программирования, которые я рассмотрю на примере конкретного языка.


    Комментарий(Comment)
    В каждом языке имеется возможность комментировать исходники, чтобы любой читающий их смог лучше понять ход ваших мыслей. Закомментированный код игнорируется компилятором/интерпретатором, поэтому на размер и скорость работы конечной программы никак не влияет. Как правило, даются 2 способа комментировать, и Lua не является исключением:



    -- комментарий для одной строки - Комментирует всю строку правее двойного минуса
    --[[ комментарий для всего что находится между двойными минусами и скобками --]] - Комментирует все, что находится между этими конструкциями(всю строку, ее часть или много строк сразу)


    Переменные(Variables)
    Именные хранители данных/значений/информации. В них записываются числа, слова, символы, данные, которые нужно хранить до востребования как результат каких-то вычислений или по каким-то другим причинам. Их можно представить как коробки с надписями, в которые вы кладете информацию, чтобы потом, в другом месте скрипта ее получить. Если старая информация в коробке больше не нужна, то туда можно положить новую, тогда прошлые данные автоматически выкинутся из коробки. Имена у переменных могут состоять из латинских букв, цифр и знака подчеркивания "_", однако начинаться с цифры они не могут. Тоесть:


    abcdefg -- Является легальной переменной
    ____lala___lala___ -- Является легальной переменной
    _ -- Является легальной переменной
    tralala__ -- Является легальной переменной
    а234a -- Является легальной переменной
    1fsf -- Не является легальной переменной из-за цифры в начале
    1234 -- Не является легальной переменной из-за цифры в начале
    !!!xx -- Не является легальной переменной из-за знаков восклицания
    ##](tt -- Не является легальной переменной из-за решеток и скобок


    Каждая переменная занимает свое именное пространство(Namespace) в пределах своей зоны действия, тоесть не может быть двух разных переменных с одинаковым именем в пределах одной зоны действия. По этому принципу в Lua разделяют переменные на 3 типа:



    Глобальные(Global) - Действительны в пределах всего скрипта/программы, следовательно они занимают именное пространство всей программы
    Локальные(Local) - Действительны только в пределах одной функции, блока или файла, взависемости от того где их объявили(как это делать рассмотрим при разборе функций), следовательно они занимают именное пространство блока, в котором их объявили
    Поля таблиц(Table Fields) - Занимают именное пространство- и действительны в пределах своей таблицы(о таблицах чуть ниже)


    Для занесения в переменную значений используется оператор "=". Например:

    a = 1 -- Заносим 1 в переменную а
    b = 1 + 2 + 3 -- Если есть какие-то операции, то сначала все они выполняются, а затем результат идет в переменную, в данном случае 6 в переменную b

    В Lua также возможно множественное присваивание(занесение) значений, тоесть:

    a,b,c = 1,2,3 -- Заносим 1 в переменную а, 2 в переменную b, 3 в переменную c
    a = 1 b = 2 с = 3 -- Тоже самое можно записать так
    -- Или вот так:
    a = 1
    b = 2
    с = 3

    Далее мы увидим, что у множественного присваивания есть очень полезные применения в функциях.

    Типы значений(Types of Values)
    Значения это та информация, которой язык позволяет манипулировать и хранить в переменных. Во многих языках, в том числе и в Lua, типы значений разделяют на примитивные и объекты. Новичку крайне не просто объяснить существенную разницу между этими понятиями, но я всеже попытаюсь, ибо это нужно знать и понимать, иначе вы можете натворить бед в своем коде. Если не вдоваться в подробности, то в Lua примитивные значения- это те, которые не имеют специального способа для их создания(например, как мы уже видели- числа просто записываются в переменные). Объекты же, наоборот, являются самобытными единицами информации, каждая из которых уникальна, и создается специальной командой(конструктором объекта). В случае с объектами, переменные содержат лишь ссылку на конкретный экземпляр в памяти, и при изменении этого экземпляра через переменную, меняется сам объект в памяти, следовательно все переменные, которые на него ссылаются будут ссылаться уже на измененный объект. Примитивные значения, наоборот, связаны с конкретной переменной. Изменяя значение этой переменной, вы не затрагиваете другие переменные, даже, если в них было копировано значение изменяемой переменной. В самом слове "примитивное" уже заключается смысл этих значений- они очень просты и отражают самый базовый уровень данных(цифра, сообщение), а объекты это сложные формы данных, часто построенные на основе примитивных значений, чтобы выполнять более сложные и конкретные функции. По ходу рассмотрения типов значений я постораюсь привести достаточно примеров, чтобы вы поняли разницу между ними.



    Примитивные значения(Primitive Types)


    Цифра(Number)
    Целое или дробное число. Примеры записи:
    10 -- Целое число
    3.43 -- Дробное число
    Числа монжо складывать, вычитать, умножать и делить друг на друга, а также отрицать и возводить в степень:

    a = 2 + 3 -- Складываем 2 и 3 и заносим результат 5 в переменную а
    a = 2 - 3 -- Вычитаем 3 из 2-х и заносим результат -1 в переменную а
    a = 2 * 3 -- Умножаем 2 на 3 и заносим результат 6 в переменную а
    a = 2 / 3 -- Делим 2 на 3 и заносим результат 0.66(6 в периоде) в переменную а
    a = - (2 + 3) -- Отрицаем сумму 2-х и 3-х и заносим результат -5 в переменную а
    a = 2 ^ 3 -- Возводим 2 в 3-ю степень и заносим результат 8 в переменную а

    При проведении с числами операций, значения в использованных переменных не меняются:

    a = -2 -- Заносим -2 в переменную а и ...
    b = -a -- ... отрицаем значение в переменной а, получая 2 и заносим результат в переменную b(фактически отрицание это умножение на -1)
    -- Число в переменной а осталось темже

    Если вы хотите, чтобы переменная получила новое значение на основе старого, то нужно провести операции с этой переменной и записать результат в нее же:

    a = 1 -- Заносим 1 в переменную а

    a = a + 1 -- Увеличиваем значение а на единицу, получая 2
    a = (a + a) * a -- Могут быть и такие интересные выражения(если мы хотим сделать сложение до умножения, то нужно ставить скобки- также, как в математике)

    Помните, что числа, являясь примитивными значениями, привязаны к конкретной переменной:

    a = 1 -- Заносим 1 в a
    b = a -- Копируем значение из a в b
    b = b + 1 -- Увеличиваем b на 1, получая 2
    -- В a и b сейчас находятся разные значения, так как изменяли мы именно значение переменной b, в которую мы копировали значение переменной a, получив 2 никак не связанных, но одинаковых значения в двух переменных

    С числами можно делать и другие операции(корень, логарифм, округление и тп), но они не "вшиты" в язык, а реализованы через простые функции и находятся в стандартной библиотеке math, которая имеется и в скрипте гомункула. Подробнее о предоставляемом ею наборе функций можно узнать тут (http://www.lua.ru/doc/5.6.html).

    Стринг(String)
    Текстовое сообщение, которое записывается между апострофов или кавычек. В стрингах разрешены не все виды знаков, так как некоторые из них зарезервированы для обозначеня специальных символов, которых нету на клавиатуре или по другим причинам. Примеры специальных символов, которые могут вам понадобиться:



    \n - Смена строки(Энтер)
    \t - Таб
    \" - Кавычка
    \' - Апостроф
    \\ - Обратный cлеш


    Примеры записи стрингов:
    'сообщение между апострофов' -- Между апострофов
    "сообщение между кавычек" -- Между кавычек
    "возможны 'апострофы' в стринге между кавычек" -- Апострофы в стринге между кавычек(если нужны кавычки, то используйте \")
    'а также "кавычки" в стринге между апострофов' -- Кавычки в стринге между апострофов(если нужны апострофы, то используйте \')
    "123" -- Число в текстовом формате
    "^,.%#$@()" -- Все символы, кроме обратного слеша обозначают сами себя
    "после этого сообщения будет нажат энтер\n" -- Знак смены строки
    "обратный слеш в конце\\" -- Так как обратный слеш занят для обозначения специальных знаков, то, чтобы написать его в стринге один раз нужно поставить двойной обратный слеш
    Стринги можно соединять с помощью оператора "..":

    a = "trala" .. " " .. "lala" -- Соединяем 3 стринга и получаем один "trala lala", который заносим в переменную а

    Данный оператор также соединяет стринги с числами, переводя последних в текстовый формат:

    a = "a " .. 1 .. " b" -- Соединяем 2 стринга и число, получая один стринг "a 1 b", который заносим в переменную а
    b = 123 .. "" -- Соединяем число 123 с пустым стрингом и получаем идентичный стринг "123"(то же число, только в текстовом формате), который заносим в переменную b

    Как и с числами, при совершении со стрингами операций, значения в переменных не меняются:

    a = "a"
    b = "b"
    c = a .. " " .. b -- Соединяем 2 стринга в переменных и пробел, получая один стринг "a b", который заносим в переменную c
    -- Cтринги в переменных а и b остались темеже

    Опять же, как и с числами, стринг привязан к конкретной переменной:

    a = "abc" -- Заносим "abc" в a
    b = a -- Копируем значение из a в b
    b = b .. "d" -- Добавляем к стрингу b стринг "d", получая "abcd"
    -- В a и b сейчас находятся разные значения, так как изменяли мы именно значение переменной b, в которую мы копировали значение переменной a, получив 2 никак не связанных, но одинаковых значения в двух переменных

    Со стрингами можно делать и другие операции(поиск, нахождение количества символов, превращение всех маленьких букв в большие и тп), но они не "вшиты" в язык, а реализованы через простые функции и находятся в стандартной библиотеке string, которая имеется и в скрипте гомункула. Подробнее о предоставляемом ею наборе функций можно узнать тут (http://www.lua.ru/doc/5.4.html).

    Буулеан(Boolean)
    Значения true/false(правда/ложь). Чем они полезны узнаем после рассмотрения всех значений.

    Пустое значение(nil)
    Специальное значение, которое обозначает отсутствие других значений(тоесть пустое значение). Если записать в переменную nil, то эта переменная автоматически удаляется и ее место именном пространстве освобождается(обратное тоже верно- любая несуществующая(неиспользованная или использованная и очищенная с помощью nil) переменная при обращение к ней будет содержать в себе nil).


    Объекты(Objects)


    Таблица(Table)
    По-сути это что-то вроде списка, который предстваляет из себя пары ключ-значение и, являясь объектом, создается специальной командой(конструктором). Значение хранится под определенным ключем, по которому это значение можно запросить у таблицы, либо записать туда новые данные. Ключи и находящиеся под ними значения могут быть значениями любого типа, тоесть под числовым ключем может быть стринг или под стринговым число(или даже таблица под табличным ключем) и тд. Пример:

    a = {} -- Команда для создания новой таблицы(конструктор)

    a[1] = "a" -- В таблицу, используя ссылку в переменной a, заносится запись, что под числовым ключем 1 будет хранится стринг "a"
    a["abc"] = 2 -- В таблицу заносится запись, что под стринговым ключем "abc" будет хранится число 2
    a[true] = false -- В таблицу заносится запись, что под буулеан ключем true будет хранится буулеан false

    b = a[1] -- Запрашиваем у таблицы, используя ссылку в переменной a, выдать запись, которая хранится под числовым ключем 1(в нашем случае это стринг "a"), и заносим ее("a") в переменную b
    c = a[2] -- Запрашиваем у таблицы, используя ссылку в переменной a, выдать запись, которая хранится под числовым ключем 2(в нашем случае такой записи нету, следовательно будет выдан nil), и заносим ее(nil) в переменную b

    Как уже было подмечено в предыдущем примере- в переменных хранится ссылка на таблицу, а не сама таблица. Самое время показать как же данная особенность объектов влияет на их использование:

    a = {} -- Создаем новую таблицу и заносим ссылку на нее в переменную a
    b = a -- Копируем ссылку из a в b

    b[1] = 2 -- Записываем в поле таблицы(ссылка на которую находится в переменной b) под индексом 1 значение 2
    -- В a и b сейчас находятся ссылки на одну и туже таблицу, следовательно все изменения через переменную b отразились в переменной a, так как мы изменяли сам объект, а не значение в переменной

    Таблицы также имеют особый синтаксис для обращения к стринговым полям(ключам):

    a = {}

    a.b = 1 -- Заносим 1 под стринговый ключ "b"
    a["b"] = 1 -- Тоже самое, только с обычным синтаксисом
    c = a.b -- Копируем единицу в переменную c

    Стринги, к которым обращаются таким способом должны соответствовать требованиям переменных, в противном случае при обращении к ним будет ошибка. Вы можете подумать, что данный синтаксис это бессмысленное излишество, но рассматривая функции я покажу, что это очень удобное дополнение.
    В Lua все глобальные переменные дублируются в таблице, которая находится в глобальной переменной _G, тоесть, например, значение в глобальной переменной a будет находиться в этой таблице под стринговым ключем "a"(_G["a"] или _G.a). Чем эта таблица полезна мы рассмотрим, опять же, в функциях.
    Таким образом, таблицы это инструмент для компактного хранения данных- что-то вроде набора переменных под единым именем, но это лишь "вершина айсберга". С помощью таблиц, в частности, можно создавать новые виды объектов. Освоив мастерство обращения с ними, вы сможете раскрыть истенную силу и потенциал Lua, однако его раскрытие выходит за рамки этого поверхностного гайда, так что двигаемся дальше.

    Функция(Function)
    Является не менее мощным инструментом, чем таблица. Если таблица позволяет объединять переменные под единым именем, то функция объединяет действия(операции) под одним именем. Функция должна иметь дефиницию/определение, тоесть, являясь объектом, как и таблица, она создается специальной командой(конструктором), внутри которой записываются действия. Пример:

    a = function() -- Создаем новую функцию и заносим ссылку на нее в переменную a

    b = 1 -- Функция будет заносить в переменную b число 1
    end

    Итак, мы ее сделали, но как теперь ее использовать? Для этого нужно где-то после определения(до определения нельзя, так как ее еще не существует в этот момент) вызвать ее:

    -- Допустим, что это продолжение прошлого куска кода и ссылка на функцию в переменной a уже существует

    a() -- Вызываем функцию через переменную a
    -- Теперь в глобальной переменной b есть число 1
    c = a -- Копируем ссылку на функцию из a в c
    c() -- Теперь и в этой переменной есть ссылка на эту же функцию, с помощью которой ее также можно вызвать

    Функция также имеет и альтернативный способ определения для упрощения чтения(ну, и, потому что в других языках функции определяют похожим способом):

    function a() -- Создаем новую функцию и заносим ссылку на нее в переменную a
    -- Для примера- это пустая функция, так как никаких действий в ней нету, следовательно при вызове ее ничего не произойдет
    end

    В прошлых примерах функции были без аргументов/параметров. Аргументы это те значения, которые мы передаем функции при вызове ее. Их тоже нужно отмечать при дефиниции функции. Например:

    function a(arg1,arg2,arg3)

    b = arg1 + arg2 + arg3 -- Функция будет заносить в переменную b сумму всех аргументов
    end

    c = 3

    a(2,с,4)
    -- В b теперь находится число 9(тоесть 2 + 3 + 4)
    -- arg1,arg2,arg3 это копии тех значений, которые мы передали функции, когда ее вызывали, следовательно, если это объекты, то изменяя их, вы изменяете сам объект через переданную ссылку

    Аргумент это локальная переменная для всего блока функции(от function до end) и занимает ее именное пространство и зону действия. Это означает, что данную переменную можно использовать только внутри этой функции, а после end она исчезает. Локальные переменные можно объявлять и самостоятельно, используя слово local:

    c = 1 -- Глобальная переменная c

    function a(arg1,arg2,arg3)

    local с -- Объявление локальной переменной c(в данном блоке она "перекрывает" глобальную переменную с темже именем, поэтому чтобы обратится к глобальной c нужно использовать ту самую глобальную таблицу _G, собственно, этим данная таблица и хороша)
    с = arg1 + arg2 + arg3 -- Далее все виды обращения и использования ее точно такие же, как и глобальных
    b = c -- Любое обращение к переменной, которую не объявляли как локальную, будет обращением к глобальной переменной
    c = c + _G.c -- Добавляем в локальную переменную c значение глобальной c
    end

    Если вы вызываете функцию с меньшим количеством агрументов, чем в дефиниции, то оставшиеся аргументы получат nil, а если с большим, то лишние значения проигнорируют:

    function a(arg1,arg2,arg3) end

    function b() end

    a(1,2) -- arg1 получит 1, arg2 получит 2, arg3 получит nil
    a(1,2,3,4) -- arg1 получит 1, arg2 получит 2, arg3 получит 3, 4-го аргумента в дефиниции нету- значение игнорируется
    a() -- arg1 получит nil, arg2 получит nil, arg3 получит nil
    b(1,2,3,4) -- Аргументы в дефиниции отсутствуют- все значения игнорируются

    Последнее характерное свойство функций- это возможность возвращать/выдавать значения. Делается это через слово return, после которого может идти перечень значений или переменных, которые функция выдаст. Как только функция "натыкается" на это слово- она сразу же прекращает свою работу и выдает значения, которые указаны после return(если такиe имеются). Пример:

    function a(arg)

    return arg -- Функция выдает свой аргумент
    end

    b = a(1) -- Функция выдаст единицу, которая запишется в переменную b
    a(1) -- Не обязательно, чтобы выдаваемое значение использовалось, в данном случае оно просто игнорируется

    function a(arg) -- Создаем новую функцию в той же переменной(старая пропадает)

    return -- Функция может ничего и не выдавать, тогда слово return будет служить лишь для того, чтобы на каком-то этапе насильно прекратить ее работу(например, когда передали аргумент не того типа- это можно проверить с помощью функции контроля, с которой мы познакомимся чуть ниже)
    end

    Теперь, самое время вспомнить про множественное присваивание, о котором я говорил в переменных, и отметил, что в функциях оно пригодится. Так вот, функция может выдавать несколько значений сразу, следовательно, чтобы все их получить нужно использовать сразу несколько переменных:

    function a(arg1,arg2,arg3)

    return arg1,arg2,arg3 -- Функция выдает свои аргументы
    end

    b,c,d = a(1,2,3) -- Функция выдаст три значения: единицу, которая запишется в переменную b, двойку, которая запишется в переменную c и тройку, которая запишется в переменную d
    b,c = a(1,2,3) -- Как в прошлом примере, не обязательно использовать все выдаваемые значения, в данном случае тройка, являясь третим значением просто игнорируется
    b,c,d,e = a(1,2,3) -- Можно использовать и больше переменных, чем количество значений, которое выдает функция, тогда лишние переменные получат nil, в данном случае это переменная e

    Итак, мы рассмотрели все основные возможности и особенности функций, но за мной осталось еще одно обещание- объяснить чем же хорош "точечный" синтаксис таблиц в контексте функций. Помните, ведь таблица является объединителем значений, а функция это тоже значение, следовательно таблица может объединять и функции. В Lua все библиотеки хранят свои функции в полях определенной таблицы, имя которой отражает суть библиотеки. Данный принцип объединения функций используется и в стандартном AI для создания комплекта функций, которые манипулируют списками(объектами, созданными на основе таблиц). Обращаться к функциям в таблицах через обычный синтаксис очень не удобно и "уродливо" в плане читаемости кода, и тут, на помощь приходит "точечный" синтаксис. Рассмотрим пример для наглядности:

    complekt = {} -- Создаем таблицу
    complekt.peremennaja = 0 -- Для примера добавляем в нее под стринг "peremennaja" число 0(фактически это переменная в таблице)

    function complekt.dajArgument(arg) -- Создаем и заносим в таблицу под стринг "dajArgument" функцию, которая всего-лишь выдает свой аргумент

    return arg
    end

    function complekt.izmeniPeremennuju(arg) -- Создаем и заносим в таблицу под стринг "izmeniPeremennuju" функцию, которая меняет значение переменной в таблице на данный аргумент

    complekt.peremennaja = arg
    end

    a = complekt.dajArgument(1) -- Выдаст единицу
    complekt.izmeniPeremennuju(2) -- Теперь в complekt.peremennaja находится 2
    complekt["izmeniPeremennuju"](3) -- Сравните верхний синтаксис с этим и вы поймете о чем я говорил

    Продолжая тему функций в таблицах, в Lua имеется особый синтаксис для обращения к объектам, которые, как я уже говорил, создаются на основе таблиц, однако таблицы это всего-лишь "каркас". Часто типичным "самодельным" объектом в Lua является таблица с набором функций в ее полях(в объект-ориентированном программировании их называют методами, тоесть функциями, привязанными к конкретному экземпляру объекта). Во многих случаях методам нужна ссылка на таблицу(объект), из которой их вызывают, и для этого был придуман этот особый синтаксис:

    a = {}

    function a:metod(arg) -- Такой дефиницией вы говорите, что данная функция будет получать таблицу, из которой ее вызвали, дополнительным(невидимым) аргументом, который будет хранится в локальной переменной self

    return self -- Данная функция выдает ссылку на таблицу, из которой ее вызвали
    end

    a:metod():metod():metod() -- Благодаря ссылке на таблицу возможно делать вот такие интересные вызовы, ведь первый вызов выдает эту же таблицу, следовательно ее можно вновь использовать для вызова любого метода

    Рассказываю я вам все это не случайно, при работе с файлами(это мы тоже рассмотрим) используется именно такой синтаксис и я не хочу, чтобы он вызывал у вас вопросы.
    Напоследок еще одна незначительная вещь, о которой я не могу не упомянуть, ибо она используется в стандартном AI. Если функции передается один аргумент, и этот аргумент конструктор таблицы({}) или литеральный("голый"- тоесть, который передается функции без переменной) стринг, то скобки при вызове функции можно не писать. Тоесть:

    function a(arg) end

    a"abc" -- Без пробела
    a "abc" -- С пробелом

    b = "abc"
    a b -- А вот так уже нельзя, ибо стринг в переменной

    a{} -- Без пробела
    a {} -- С пробелом

    b = {}
    a b -- Тоже нельзя, ибо таблица в переменной

    Как и в случае с особым синтаксисом таблиц, вы можете подумать, что это бессмысленное дополнение, однако в случаях со стрингами оно может повысить читаемость, а в случае с таблицами я просто не объяснял вам возможностей их конструктора- в них вся суть(если говорить конкретнее, то конструктором таблицы в аргументе можно симулировать именные аргументы), но, как я уже говорил- это вне базового контекста гайда.

    Userdata, Thread(Coroutine)
    Не важны в контексте гайда и практически бесполезны в скрипте гомункула.
    Восемь, Arny, -=FaNaTiK=- и ещё 1-му нравится это.
  2. maas

    maas Игрок

    Операторы сравнения и логики(Comparative and Logical Operators)
    Используются для сравнения значений двух переменных:



    a == b - a равно b
    a ~= b - a не равно b
    a >= b - a больше, либо равно b
    a <= b - a меньше, либо равно b
    a > b - a больше b
    a < b - a меньше b


    Данные выражения называются условиями, их результат всегда либо true, либо false. Условия можно комбинировать, чтобы получить более сложные выражения. Для этого используют логические операторы or(или) и and(и). or между двумя выражениями означает, что если одно, либо другое, либо оба из них true, то и вся комбинированная конструкция возвратит тоже true, в противном случае false. and между двумя выражениями, наоборот, означает, что, только если оба условия true, то и вся конструкция true. Хочу также отметить, что or надо рассматривать как логическое сложение, а and как логическое умножение(тоесть, например, выражение "a and (b or c)" будет идентично выражению "a and b or a and c"). Сравнение разных типов значений оператором "==" всегда выдаст false, оператором "~=" всегда true, а при попытке сравнить их операторами больше-меньше будет ошибка. Фактически, операторами больше-меньше можно сравнивать только числа(здесь пояснений не нужно) и стринги(в этом случае сравнивается старшенство символов, которое определяется настройками региона и языка в нашей операционной системе; если рассматривать стандартный латинский алфавит, то "a" будет меньше "b" и тд). Пример:
    a = (2 == 2 and 3 > 4 or "blabla" == "bla" .. "bla" or "3" == 3)
    Как и умножение, так и and(если не стоит уточняющих скобок), являясь логическим умножением, рассматривается первым. 2 == 2 выдаст true, однако 3 > 4 выдаст false, следовательно общий результат первого "слогаемого" будет false. "blabla" == "bla" .. "bla" выдаст true, так как после соединения(сначала производятся все операции, если они есть, а потом уже сравневаются результаты) правый стринг будет идентичен левому. Последнее условие выдаст false, так как при сравнении разных типов значений результат всегда будет false, даже если это цифра и стринг с этой же цифрой. Итак, в конечном итоге мы имеем:
    a = (false or true or false)
    Как минимум одно из "слогаемых" true, следовательно все выражение будет тоже true и в переменную а запишется true.
    В Lua операции сравнения не являются единственной возможностью задавать условия. Любое значение, отличное от false и nil, выдаст true при попытке использовать его как условие. Имеется также логический опрератор not(не), который, если поставить его перед значением, наоборот, выдаст true, если значение false или nil. Пример:




    a = 1
    c = (a and not b)
    -- В a значение, отличное от false и nil, следовательно оно возвратит true
    -- b мы не определяли, следовательно в ней находится nil, а в случае с not условие выдаст true
    -- Таким образом получаем true and true, что выдаст true и результат запишется в переменную c
    c = not (a == b) -- not, в принципе, можно использовать для отрицания любого выражения, которое выдает буулеан, он просто меняет значение буулеана на противоположное, в данном случае в c запишется true
    c = not true -- А тут в c запишется false

    Возвращаясь к объектам, объект может быть равен только самому себе, тоесть значения переменных с объектами равны только если они ссылаются на один и тот же объект.

    Приоритет операций(Precedence)
    Определяет какая операция следует за какой:



    ^
    not - (отрицание)
    * /
    + -
    ..
    < > <= >= ~= ==
    and
    or


    Первой выполняется возведение в степень и тд. Используя скобки, вы можете изменить очередность действий(например, сделать сложение до умножения).

    Функция контроля/условия(Control Statement)
    Функция, которая решает выполнять ли указанный кусок кода исходя из данного ей условия(тоесть она выполняет его, если условие true). В Lua(как наверно и в любом языке) такая функция называется if(если). Часто она используется в связке с else(в противном случае) или elseif(в противном случае, если). Функция может иметь следующие структуры:



    if условие then
    код
    end
    код выполняется, если условие true.
    if условие then
    код1
    else
    код2
    end
    код1 выполняется, если условие true, в противном случае выполняется код2.
    if условие1 then
    код1
    elseif условие2 then
    код2
    elseif условие3 then
    код3
    -- здесь может быть сколько угодно альтернативных условий
    elseif условие(n) then
    код(n)
    end
    код1 выполняется, если условие1 true, в противном случае проверяется следующее условие, пока программа не натыкается на условие, которые даст true, либо пока не дойдет до конца блока(end). Выполняется всегда только одно условие из всей конструкции- то, которое первым выдаст true.
    if условие1 then
    код1
    elseif условие2 then
    код2
    elseif условие3 then
    код3
    -- здесь может быть сколько угодно альтернативных условий
    elseif условие(n) then
    код(n)
    else
    код(n + 1)
    end
    Полностью повторяет прошлый вариант, но с одним дополнением- если ни одно из n условий не выдало true, то выполнится код(n + 1).


    В функции контроля, как и в любом другом блоке, можно самостоятельно объявлять локальные переменные(см. функции). Причем каждая из частей(от if до esle или от esle до end или от одного elseif до другого elseif) является отдельным блоком.

    Цикл(Loop)
    Функция, которая повторяет указанных кусок кода, пока определенное условие действительно. Тоже является разновидностью функций условия/контроля, однако если if проверял только 1 раз, то цикл проверяет и выполняет много раз(в цикле) пока условие действительно. В Lua имеются 4 вида циклов:



    while условие do
    код
    end
    Если условие true, то код выполняется. Затем условие снова проверяется и, если оно все еще true, то код выполняется опять и тд.
    repeat код
    until условие

    Тоже самое, что и прошлый цикл, только сначала один раз выполняется код, а потом проверяется условие(по непонятным мне причинам оно должно быть false, чтобы цикл продолжался- будем считать это исключением из правил циклов). Таким образом, код выполняется как минимум один раз независемо от условия.
    for переменная = начальное значение, конечное значение, шаг do
    код
    end
    Цикл с числовым условием повтора. Берется переменная(имя вы сами выбираете), в которую записывается начальное значение ввиде числа(данная переменная становится локальной для данного блока). Далее выбирается конечное значение, при достижении или привышении которого переменной цикл прекращается. Шаг определяет на сколько будет переменная изменятся с каждой "прокруткой" цикла, тоесть сколько туда будет прибавляться или оттуда отниматься(в зависемости от того с каким знаком шаг). Если не записывать шаг, то он автоматически будет 1.
    Чаще всего для переменной выбирают короткие имена из одного символа(например, i или j), так как смысловой нагрузки они не несут и длину их записи лучше сократить до минимума. Пример:
    a = 0 -- Заносим 0 в переменную а

    for i = 1, 10 do -- Делаем цикл от 1 до 10 с шагом 1(по-умолчанию 1)
    a = a + i -- Каждый раз прибавляем в а значение i, тоесть 0 + 1 = 1, затем 1 + 2 = 3 и тд
    end

    -- На выходе получаем 55 в переменной а
    for переменная(ые) in функция итерации do
    код
    end
    Я не стану вам объяснять что такое итерация и каким образом функционирует этот цикл, ибо это превратится в длинное и не нужное запудривание мозгов. При разборе скрипта мы обязательно вернемся к этому циклу и посмотрим что он делает и чем он полезен, так как только его там и используют, и вы, наверняка, тоже не будете исключением. Использовать его очень просто и удобно, и для этого не обязательно знать как он действует изнутри.


    Часто цикл нужно прервать на этапе, когда вы уже нашли что искали, перебирая какой-нибудь набор данных. Для этого используется break. Как только цикл "натыкается" на это слово- он моментально прекращается. break также используется для прекращения бесконечных циклов. Например(здесь используется while, но и любой другой цикл можно сделать бесконечным):
    i = 0

    while true do -- Это бесконечный цикл, так как условие всегда true
    -- Если не продумать путей выхода из него, то на нем ваш скрипт, вместе с клиентом Рагнарока, зависнут
    i = i + 1 -- Каждый раз прибавляем 1 в i

    if i == 100 then -- На сотенке ...
    break -- ... прекращаем цикл
    end
    end

    -- Конечно, настолько глупые циклы никто не делает, но это просто примеры
    В цикле, как и в любом другом блоке, можно самостоятельно объявлять локальные переменные(см. функции).

    Блоки и правила видения(Blocks and Scopes)
    Мне показалось, что я всеже не достаточно рассказал о блоках и принципах видения переменных в них, поэтому решил обобщить блоки в отдельный пункт.
    Каждый блок(файл, цикл, функция условия, простая функция или особый вид блока, о котором я расскажу чуть ниже) имеет свое именное пространство, и каждая переменная, которая была объявлена в нем как локальная, занимает свое место в этом блоке, тоесть получает уникальное имя, по которому к ней можно обратиться. Вместе с именем, у локальной переменной есть и зона, в пределах которой она доступна. Файл тоже можно рассматривать как блок, поэтому локальные переменные можно объявлять и прямо внутри файла, как мы до того делали с глобальными переменными, только используя слово local, но такие переменные не будут видны в других файлах(если ваш скрипт состоит из нескольких файлов). Если в блоке есть еще один блок, то все локальные переменные будут видны в этом внутреннем блоке и тд. Глобальные же переменные видны во всех блоках(следовательно и файлах) скрипта, и для их объявления не нужны какие-то специальные методы, так как любое обращение к несуществующей в блоке локальной переменной будет обращением к глобальной переменной(и как мы уже знаем, если мы до этого с ней ничего не делали, то в ней будет nil). Например:
    local a = 1 -- Локальная переменная для всего файла
    b = 2 -- Глобальная переменная для всего скрипта(всех файлов)

    if a == 1 then
    a = a + 1 -- a видно тут и ее можно использовать
    b = b + 1 -- Глобальные переменные видно везде
    local c = 5 -- Локальная переменная для данного блока if
    local d = 4

    if b == 3 then
    local e = 6
    local d = 5 -- Данная локальная переменная перекрывает локальную переменную прошлого блока, так как у нее такое же имя, поэтому обратиться внутри этого блока ко внешней d мы не можем

    с = с + 1 -- c видно тут и ее можно использовать
    c = c + d -- Тут используется внутренняя d со значением 5, так как она перекрыла внешнюю
    a = a + b -- a и b также доступны тут
    end
    -- c все еще видно тут, а вот e и d из прошлого блока для нас уже потеряны
    d = d + 1 -- А вот d из текущего блока все еще с нами и выйдя из предыдущего блока мы снова можем ее использовать
    end

    -- Итого, в a число 5, в b число 3, а остальные переменные уже не видно и они для нас утеряны
    -- Этот пример наверняка очень сложно читать и понимать, однако он наглядно демонстрирует большинство сценариев доступа к переменным разных блоков, а также конфликт с одноименными переменными
    Есть также особый вид блока, который служит единственной цели- создание блока(на самом деле код, который загружается из файла тоже искусственно помещается в данный блок, поэтому файл и считается блоком):
    local a = 1

    do -- Блок do
    local b = 2
    local c -- Если объявлять локальную переменную без присваивания ей значения, то в ней будет nil
    c = 1
    b = b - 1
    a = a + 1
    c = d -- Глобальную d мы ни разу не использовали, следовательно там nil, который и запишется в c
    end

    -- Здесь b и c уже вне зоны видимости и обратиться к нему мы уже не можем, а вот a все еще с нами со значением 2

    Работа с файлами(File Access)
    Наверное, любой язык предоставляет возможности создавать/открывать файлы и считывать из них информацию, Lua здесь не является исключением.



    io.open(path , mode)

    Аргументы: path - путь в файлу, mode - стринг, определяющий способ обращения к файлу
    Возвращает: Объект доступа к файлу(в последующих функциях он обозначается как file)
    Описание: Открывает доступ к файлу и выдает его ввиде объекта для дальнейшей работы. Путь к файлу прописывается также, как и в Windows, например "C:\RO\AI\myfile.txt"(понятен вариант и со слешами "C:/RO/AI/myfile.txt") или "./AI/myfile.txt"(точка в данном случае обозначает папку вашего клиента). Имеются следующие способы обращения к файлу(виды стрингов mode):



    "r" - Файл открывается для чтения(если файла нет, то возвращает nil)
    "w" - Файл открывается/создается для записи(вся предыдущая информация сохраняется)
    "a" - Файл открывается/создается для дополнения(вся предыдущая информация стирается)


    Если не уточнять способ доступа(тоесть передавать только первый аргумент), то он автоматически будет "r".


    file:read(mode)

    Аргументы: file - объект доступа к файлу, mode - стринг, определяющий способ считывания из файла
    Возвращает: 1 Значение, взависемости от mode
    Описание: Считывает из файла данные. Считывать можно следующими способами(виды стрингов mode):



    "*n" - Считывает одно число
    "*a" - Считывает все содержимое файла и выдает ввиде одного стринга
    "*l" - Считывает одну строку файла и выдает ввиде стринга
    number - Считывает number символов из строки


    Если не уточнять способ считывания(тоесть вообще не передавать аргументов), то он автоматически будет "*l".


    file:write(arg)

    Аргументы: file - объект доступа к файлу, arg - стринг или число
    Возвращает: Ничего
    Описание: Записывает стринг или число в файл.


    file:close()

    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Закрывает доступ к файлу. В принципе, файл можно и не закрывать, однако новый доступ открыть к нему будет нельзя, пока старый не закрыт.


    Пример:
    value = 1 -- Заносим 1 в переменную value

    local myfile = io.open("C:/myfile.txt","w") -- Создаем(или открываем, если он уже существует) для записи("w") файл myfile.txt на диске C и помещаем объект доступа к нему в переменную myfile

    myfile:write(value) -- Записываем туда 1(причем, если в файле что-то было, то оно сотрется из-за того, что способ обращения "w")
    myfile:close() -- Закрываем доступ к файлу

    myfile = io.open("C:/myfile.txt","r") -- Открываем для чтения файл myfile.txt на диске C и помещаем объект доступа к нему в переменную myfile

    local a = myfile:read("*n") -- Считываем оттуда одно число(в нашем случае это 1), которое помещаем в переменную a
    myfile:close() -- Закрываем доступ

    С полным перечнем функций для работы с файлами можно ознакомиться тут (http://www.lua.ru/doc/5.7.html).

    Ошибки(Error Handling)
    Интерпретатор не может сделать из вашего кода программу, если в нем имеются ошибки, следовательно он должен сообщать вам о них, чтобы вы смогли их исправить. Если в вашем скрипте будет синтаксическая ошибка, то при заходе в игру и вызове гомункула, клиент выдаст ошибку. В окне ошибки будет сказано в какой части какой строчки какого файла она произошла(если окно будет постоянно появляться при нажатии OK, то избавится от него можно закрыв клиент через Alt + F4, Ctrl + Alt + Delete или, нажимая Enter, свернув гомункула между вылетами окна ошибки). Существуют также, так называемые, баги- ошибки, которые проявляются только при определенных условиях(клиент на нее отреагирует также, если произойдет событие, которое ее спровоцирует). Таким образом, проверку на ошибки можно сравнить со спеллером- проверяется правильность написания слов, но не смысл написанного. Чтобы каждый раз не проверять работоспособность своего скрипта через клиент, имеется программа lua.exe в папке вашего клиента(если у вас ее нету, значит у вас довольно старый клиент). Она проверяет скрипт на синтаксические ошибки и также выдает строчку, место и файл с ошибкой. Если она ничего не выдала, значит ваш скрипт не имеет синтаксических ошибок(баги могут быть). Чтобы запустить проверку скрипта, нужно написать в строку окна lua.exe:
    dofile "./AI/AI.lua"
    Конечно, если ваш файл находится в папке USER_AI, то надо написать:
    dofile "./AI/USER_AI/AI.lua"

    Стандартные библиотеки(Standard Libraries)
    В Lua имеются следующие библиотеки:



    Базовая библиотека (http://www.lua.ru/doc/5.1.html)
    Библиотека для создания и обслуживание объектов thread типа (http://www.lua.ru/doc/5.2.html) - В таблице coroutine
    Библиотека модулей (http://www.lua.ru/doc/5.3.html) - В таблице package(кроме функции require)
    Стринговая библиотека (http://www.lua.ru/doc/5.4.html) - В таблице string
    Табличная библиотека (http://www.lua.ru/doc/5.5.html) - В таблице table
    Математическая библиотека (http://www.lua.ru/doc/5.6.html) - В таблице math
    Библиотека ввода-вывода (http://www.lua.ru/doc/5.7.html) - В таблице io
    Библиотека операционной системы (http://www.lua.ru/doc/5.8.html) - В таблице os
    Библиотека отладки (http://www.lua.ru/doc/5.9.html) - В таблице debug


    Серым отмечены те библиотеки, которых нету в скрипте гомункула(кроме функции require- она есть). Я не стал расписывать их сам, вместо этого дал ссылки на мануал, из-за чего могут быть небольшие расхождения ввиду разности версий Lua в мануале и клиенте(см. главу Примечание в конце гайда для пояснений).

    ---

    V. Редактор исходников SciTE

    Итак, мы ознакомились с языком и, первым делом, перед разбором скрипта, нужно установить хороший текстовый редактор, который сможет понимать код Lua. В принципе, это не обязательно, но он очень хорошо помогает читать скрипт, так как выделяет различные его элементы разными цветами и шрифтами. Я советую установить SciTE, который можно скачать по ссылке в начале гайда. Приемущество этого редактора в простоте и функциональности. Он легок в использовании, понимает практически все современные языки и очень удобно подсвечивает элементы скрипта.
    Установив редактор, тестируем его. Откроем папку AI в вашем клиенте. Если он у вас асгардовский, то там должно быть 3 файла с расширением ".lua" и 1 с ".htm", а также папка USER_AI. Нас конечно же интересуют файлы ".lua".
    Чтобы впредь с ними было легче работать, ассоциируем их с нашим новым текстовым редактором SciTE:


    Кликаем правой кнопкой мыши по файлу Const.lua и выбираем Open With -> Choose Program...(Открыть с помощью -> Выбрать программу...)
    Ставим галочку на Always use the selected program to open this kind of file(Использовать ее для всех файлов такого типа)
    Выбираем SciTE из списка или находим через Browse(Обзор)
    И вот, перед нами первый файл AI гомункула, который мы будем рассматривать в следующей главе

    ---

    VI. Функции и команды в AI

    Const.lua - Изучение базовых команд для управления гомункулом и функций для получения информации об окружающей гомункула среде:

    Благополучно открыв Const.lua через SciTE в прошлой главе, мы видим перед собой полоску наклонного зеленого текста. В нем указаны базовые функции, которыми разработчики гомункулов одарили нас, дабы мы могли управлять нашими питомцами, а также получать информацию об окружающей их обстановке. Да, именно на этом скудном наборе и строится каждый AI, начиная со стандартного, заканчивая MirAI и RampageAI, поэтому вы наверняка поняли, что люди, сотворившие эти весьма не простые скрипты имели весьма не скудную фантазию[​IMG]
    Перейдем к разбору:


    TraceAI (arg)
    Аргументы: arg - стринг или число
    Возвращает: Ничего
    Описание: Данная функция предназначена для записывания логов, по которым отслеживается ход работы вашего скрипта, но ее можно использовать и для любых других целей. Запись идет в файл TraceAI.txt(в папке клиента), однако она не начнется, пока не будет включена командой /traceai. Повторный ввод команды отключит запись. В принципе, не очень полезная функция, так как в Lua имеются средства манипулирования файлами, которыми вы можете выбрать и место сохранения и название файла по-своему усмотрению(более того, данная функция еще и каждый раз при записи делает что-то вроде временного штампа с корейскими символами).

    MoveToOwner (id)
    Аргументы: id - ID вашего гомункула.
    Возвращает: Ничего
    Описание: Команданая функция, которая приказывает гомункулу идти к хозяину(на одну клетку ниже). Она не является базовой, так как ее можно легко воссоздать, используя другие базовые функции.

    Move (id , x , y)
    Аргументы: id - ID вашего гомункула, x,y - координаты клетки
    Возвращает: Ничего
    Описание: Базовая командная функция, которая приказывает вашему гомункулу идти на указанную клетку. Если клетка вне зоны видимости, то ничего не произойдет.

    Attack (id1 , id2)
    Аргументы: id1 - ID вашего гомункула, id2 - ID цели
    Возвращает: Ничего
    Описание: Базовая командная функция, которая приказывает вашему гомункулу атаковать цель.



    GetV (V_ , id)
    Аргументы: V_ - аргумент, который определяет какую информацию об объекте должна выдать функция, id - ID объекта
    Возвращает: 1 или 2 целых числа
    Описание: Базовая функция, которая выдает информацию об окружающих гомункула объектах. На этой функции я хочу остановится подробнее, так как она является ключевой в постоении AI. Также в ней используется принцип именования констант, которые какраз и хранятся в файле Const. lua .
    Первый аргумент функции это число от 0 до 12, которое определяет что должна выдать функция. Если вернуться к нашему файлу, то ниже комментария с функциями имеется список, в котором этим числам даны имена, дабы сделать скрипт более понятным. Например, чтобы не писать цифру 0, мы пишем вместо нее V_OWNER и читающему сразу становится понятно, что эта функция возвратит ID владельца гомункула, тоесть ID вашего алхимика. В этом принципе нет ничего сложного, но он позволяет сделать код более читаемым и понятным. А теперь рассмотрим подроднее чтоже возвращает нам функция при каждом аргумента:

    ВНИМАНИЕ!
    Если объект находится вне зоны видимости, то функция возвратит -1. Если объект находится в зоне видимости, но скрыт от глаз с помощью Cloacking/Hiding/Chase Walk, то функция выдаст о нем любую информацию, кроме координат.



    GetV (0|V_OWNER , id)
    Возвращает: 1 целое число
    Описание: Выдает ID вашего алхимика/креатора. Аргумент id может быть любым числом.

    GetV (1|V_POSITION , id)
    Возвращает: 2 целых числа
    Описание: Выдает x,y координаты клетки, на которой стоит объект. В случае отсутствия объекта в зоне видимости(или, если объект находится в зоне видимости, но скрыт от глаз с помощью Cloacking/Hiding/Chase Walk), будет выдан -1, а второго значения не будет.

    GetV (2|V_TYPE , id)
    Возвращает: 1 целое число
    Описание: На данный момент это бесполезная функция. Она выдает id, которое ей дали аргументом, тоесть никаких преобразований не производит.

    GetV (3| V _ MOTION , id)
    Возвращает: 1 целое число
    Описание: Выдает действие(состояние спрайта/картинки) объекта ввиде числа. У чисел есть свои имена, с которыми можно ознакомиться в файле Const. lua . По непонятным причинам все действия гомункула, кроме движения, отображаются как "Стоит".



    0|MOTION_STAND - Стоит
    1|MOTION_MOVE - Движется
    2|MOTION_ATTACK - Атакует
    3|MOTION_DEAD - Мертв
    4 - Получает урон
    5 - Нагибается
    6 - Сидит
    7 - Использует скилл
    8 - Кастует скилл
    9|MOTION_ATTACK2 - Удар друмя руками(ассассины)
    10 - ?
    11 - ?
    12 - Кидает
    13 - Стойка Auto Counter найтов
    14 - ?
    15 - ?
    16 - Танцует(дансер)
    17 - Играет песню(бард)
    18 - Каст скилла Sharp Shooting
    19 - Каст скилла High Jump
    20 - Стойка после скилла High Jump
    21 - Стойка после скилла Prepare Whirlwind Kick
    22 - Стойка после скилла Prepare Axe Kick
    23 - Стойка после скилла Prepare Round Kick, а также стойка после каста спирита у линкеров
    24 - Стойка после скилла Prepare Counter Kick
    25 - Нагибание у линкеров
    26 - Использование скилла Counter Kick
    27 - Использование скилла Flying Side Kick
    28 - Каст скиллов Acid Demonstration и Slim Potion Pitcher, а также стойка после них
    29 - ?
    30 - Использование скилла Whirlwind Kick
    31 - Использование скилла Axe Kick
    32 - Использование скилла Round Kick
    33 - Стойка после скилла Comfort of Sun/Moon/Stars
    34 - Стойка после скилла Warmth of Sun/Moon/Stars
    35 - Нагибание у нинзь
    36 - Использование скиллов у нинзь
    37 - Каст у нинзь
    38 - Стойка с пистолетами после скиллов Fling, Triple Action, Adjustment, Increase Accuracy и Desperado
    39 - Стойка после скилла Flip Coin
    40 - Стойка с гаттлингом после скиллов Cracker и Ground Drift
    41 - Каст у ганслингеров
    42 - Каст скилла Ground Drift


    GetV (4|V_ATTACKRANGE , id)
    Возвращает: 1 целое число
    Описание: Выдает дальность атаки объекта.

    GetV (5|V_TARGET , id)
    Возвращает: 1 целое число
    Описание: Выдает ID последней цели, которую объект атаковал, либо на которую кастовал атакующий или суппорт скилл(цель фиксируется в момент начала каста). При отсутствии таковой выдает 0. Факт атаки должен быть в поле вашего обзора, иначе функция его не зафиксирует. Также, при выходе объекта за пределы вашего экрана и возвращении обратно, его цель вновь станет 0(если таковая была).

    GetV (6|V_SKILLATTACKRANGE , number , id)
    Аргументы: number - любое целое число, id - ID скилла гомункула
    Возвращает: 1 целое число
    Описание: Выдает дальность атаки скиллов(почему-то даже пассивных и суппортовых) вашего текущего гомункула(на другие скиллы выдает 1). Очень странная функция, так как имеет 3 аргумента, причем number может быть любым числом, которое никак не влияет на выдаваемые функцией значения.

    GetV (7|V_HOMUNTYPE , id)
    Возвращает: 1 целое число
    Описание: Выдает тип гомункула ввиде числа. У чисел есть свои имена, с которыми можно ознакомиться в файле Const. lua ( _H означает эволюционировавшую форму).
    Но у этой функции есть еще 2 интересных оссобенности- если объект монстр, то она выдает его вид(ввиде четырехзначного числа, которое, например, использует MirAI), а если объект игрок, то она выдает его профессию(ввиде двух- или четырехзначного числа). Список ID профессий(на самом деле это просто все виды спрайтов игроков, поэтому, например, рыцарь на пеко и просто рыцарь обозначаются отдельно):



    0 - Novice
    1 - Swordman
    2 - Magician
    3 - Archer
    4 - Acolyte
    5 - Merchant
    6 - Thief
    7 - Knight
    8 - Priest
    9 - Wizard
    10 - Blacksmith
    11 - Hunter
    12 - Assassin
    13 - Knight на Peco Peco
    14 - Crusader
    15 - Monk
    16 - Sage
    17 - Rogue
    18 - Alchemist
    19 - Bard
    20 - Dancer
    21 - Crusader на Grand Peco Peco
    22 - Любая профессия в свадебном наряде
    23 - Super Novice
    24 - Gunslinger
    25 - Ninja
    26 - Любая профессия в костюме Санты
    27 - Любая профессия в летнем костюме
    4001 - High Novice
    4002 - High Swordman
    4003 - High Magician
    4004 - High Archer
    4005 - High Acolyte
    4006 - High Merchant
    4007 - High Thief
    4008 - Lord Knight
    4009 - High Priest
    4010 - High Wizard
    4011 - Whitesmith
    4012 - Sniper
    4013 - Assassin Cross
    4014 - Lord Knight на Peco Peco
    4015 - Paladin
    4016 - Champion
    4017 - Professor
    4018 - Stalker
    4019 - Creator
    4020 - Clown
    4021 - Gypsy
    4022 - Paladin на Grand Peco Peco
    4023 - Baby Novice
    4024 - Baby Swordman
    4025 - Baby Magician
    4026 - Baby Archer
    4027 - Baby Acolyte
    4028 - Baby Merchant
    4029 - Baby Thief
    4030 - Baby Knight
    4031 - Baby Priest
    4032 - Baby Wizard
    4033 - Baby Blacksmith
    4034 - Baby Hunter
    4035 - Baby Assassin
    4036 - Baby Knight на Peco Peco
    4037 - Baby Crusader
    4038 - Baby Monk
    4039 - Baby Sage
    4040 - Baby Rogue
    4041 - Baby Alchemist
    4042 - Baby Bard
    4043 - Baby Dancer
    4044 - Baby Crusader на Grand Peco Peco
    4045 - Baby Super Novice
    4046 - Taekwon
    4047 - Star Gladiator
    4048 - Star Gladiator в Юнионе(летающий)
    4049 - Soul Linker


    GetV (8|V_HP , id)
    Возвращает: 1 целое число
    Описание: Выдает текущее ХП гомункула или хозяина(на остальных выдает -1).

    GetV (9|V_SP , id)
    Возвращает: 1 целое число
    Описание: Выдает текущее СП гомункула или хозяина(на остальных выдает -1).

    GetV (10|V_MAXHP , id)
    Возвращает: 1 целое число
    Описание: Выдает максимальное ХП гомункула или хозяина(на остальных выдает -1).

    GetV (11|V_MAXSP , id)
    Возвращает: 1 целое число
    Описание: Выдает максимальное СП гомункула или хозяина(на остальных выдает -1).

    GetV (12|V_MERTYPE , id) -> У нас пока не введено
    Возвращает: 1 целое число
    Описание: Выдает тип наемника ввиде числа. У чисел есть свои имена, с которыми вы можете ознакомиться в файле Const. lua .


    GetActors ()
    Аргументы: Отсутствуют
    Возвращает: 1 таблицу
    Описание: Базовая функция, которая выдает таблицу, содержащую ID всех(даже тех, кто в Cloacking/Hiding/Chase Walk) объектов(ваш персонаж, ваш гомункул, другие игроки, другие гомункулы, монстры, петы, наемники, NPC, порталы между локациями) в зоне видимости. Все объекты, в произвольном порядке, занимают в таблице ключи от 1 до их количества, однако хозяин и гомункул всегда занимают последние 2 ключа(тоесть, если на экране помимо гомункула и хозяина еще 3 объекта, то эти объекты займут ключи от 1 до 3, гомункул будет в 4-ом, а хозяин в 5-ом). Таким образом, в таблице всегда имеются гомункул и хозяин, а если вокруг них есть еще объекты, то они их "сдвигают вниз" по таблице.

    GetTick ()
    Аргументы: Отсутствуют
    Возвращает: 1 целое число
    Описание: Базовая функция, которая выдает время в миллисекундах.
    Восемь, Arny, -=FaNaTiK=- и ещё 1-му нравится это.
  3. maas

    maas Игрок

    GetMsg (id)
    Аргументы: id - ID вашего гомункула
    Возвращает: 1 таблицу
    Описание: Базовая функция, которая выдает таблицу с числовым значением команды, посланной гомункулу через Alt + Click или Alt + T, вместе с сопутствующими ей значениями(если такие имеются). У всех числовых значений команд есть свои именные переменные, список которых можно увидеть в конце файла Const.lua до большого комментария, а в самом комментарии перечислены этиже команды с корейским объяснением сопутствующих им значений. Перейдем к конкретному толкованию вышеизложенного:



    Когда никаких команд не поступает, возвращаемая таблица содержит:


    Под ключем 1 число 0|NONE_CMD


    Когда вы зажимаете Alt и кликаете по клетке, возвращаемая таблица содержит:


    Под ключем 1 число 1|MOVE_CMD
    Под ключем 2 x координата клетки
    Под ключем 3 y координата клетки


    Когда вы зажимаете Alt и кликаете 2 раза по цели, возвращаемая таблица содержит:


    Под ключем 1 число 3|ATTACK_OBJECT_CMD
    Под ключем 2 ID цели


    Когда вы алхимиком/креатором используете скиллы Caprise, Moonlight или S. B. R. 44, возвращаемая таблица содержит:


    Под ключем 1 число 7|SKILL_OBJECT_CMD
    Под ключем 2 уровень скилла
    Под ключем 3 ID скилла
    Под ключем 4 ID цели


    Когда вы нажимаете Alt + T, возвращаемая таблица содержит:


    Под ключем 1 число 9|FOLLOW_CMD



    За один вызов функции AI(одну ее "прокрутку") команду можно считать лишь один раз, после чего функция всегда будет выдавать NONE_CMD.
    Остальные команды пока не реализованны в клиенте, тоесть нет кнопок, которые бы посылали эти команды гомункулу. Скорее всего они так и останутся невостребованными.

    GetResMsg (id)
    Аргументы: id - ID вашего гомункула
    Возвращает: 1 таблицу
    Описание: Базовая функция, которая выдает таблицу с числовым значением команды, посланной гомункулу через Alt + Shift + Click, вместе с сопутствующими ей значениями(если такие имеются). Принцип тотже, что и у предыдущей функции:



    Когда никаких команд не поступает, возвращаемая таблица содержит:


    Под ключем 1 число 0|NONE_CMD


    Когда вы зажимаете Alt + Shift и кликаете по клетке, возвращаемая таблица содержит:


    Под ключем 1 число 1|MOVE_CMD
    Под ключем 2 x координата клетки
    Под ключем 3 y координата клетки


    Когда вы зажимаете Alt + Shift и кликаете 2 раза по цели, возвращаемая таблица содержит:


    Под ключем 1 число 3|ATTACK_OBJECT_CMD
    Под ключем 2 ID цели



    В стандартном AI(в MirAI тоже) эти команды записываются в специальный список, откуда потом по-очереди считываются и исполняются(см. ГОМУНКУЛЫ :: Полное Руководство -> Общие сведения -> Управление гомункулом). Со списками мы познакомимся чуть позже в файле Util.lua, и с их применением в AI.lua.

    SkillObject (id , level , skill , target)
    Аргументы: id - ID вашего гомункула, level - уровень скилла, skill - ID скилла, target - ID цели
    Возвращает: Ничего
    Описание: Базовая командная функция, которая приказывает гомункулу использовать направленный скилл гомункула по цели. Хотя название и аргументы подразумевают направленные скиллы, с помощью этой функции можно использовать любые скиллы гомункулов. В случае, если это скилл обкаста гомункула, то целью будет гомункул. Если же это обкаст хозяина и гомункула, либо наземный Bio Explosion скилл, то целью может быть любой из них.

    SkillGround (id , level , skill , x , y)
    Аргументы: id - ID вашего гомункула, level - уровень скилла, skill - ID скилла, x,y - координаты клетки
    Возвращает: Ничего
    Описание: Базовая командная функция, которая приказывает гомункулу использовать наземный скилл гомункула по клетке с координатами x,y в пределах зоны видимости. На данный момент она бесполезна.

    IsMonster (id)
    Аргументы: id - ID объекта
    Возвращает: 0 или 1
    Описание: Если объект монстр, то возвращает 1, если нет то 0. Данная функция не является базовой, так как может быть продублированна другими методами.


    Const.lua пройден, функции и команды рассмотрены- пора переходить к следующему файлу.

    Util.lua - Первые практические примеры применения функций, ознакомление с комплектом функции для создания и обслуживания списков:

    Начинаем, как всегда, с того, что открываем Util.lua через SciTE. С первой же строчки мы видим следующее:
    require "./AI/Const.lua"
    require это функция из стандартной библиотеки Lua, с помощью которой осуществляется подключение дополнительных файлов с кодами к основному скрипту. Синтаксис путя к файлу тотже, что и у функции обращения к файлу io.open(с которой мы уже знакомы). Это единственная функция в скрипте, которую используют без скобок(поэтому я вам и объяснял в каких случаях это возможно) из-за того, что в ней удобно использовать литеральные стринги, так как сразу видно какой файл подключается, а без скобок это еще проще увидеть.
    Далее идет набор функций(который гордо называется утилитой List), одна и которых создает списки, а остальные их обслуживают(записывают, считавают, очищают). Записывать можно как в правую, так и в левую стороны списка, также как и с считывать с них(после считывания значение удаляется из списка). Тоесть, например, если вы по-очереди занесли 2 значения(3, затем 5) в левую сторону списка, то при считывании крайнего значения с правой вы получите 5, и в списке останется только 3.
    В стандартном AI, с помощью этой утилиты, создается список ResCmdList, который служит для хранения команд MOVE_CMD и ATTACK_OBJECT_CMD, посылаемых через Alt + Shift + Click, выстраивая таким образом цепочку последовательных действий. Рассмотрим же, чем каждая из функций занимается:


    List.new ()
    Аргументы: Отсутствуют
    Возвращает: Пустой список(объект на основе таблицы)
    Описание: Создает и выдает пустой список.

    List.pushleft (list , value)
    Аргументы: list - список, value - любое значение
    Возвращает: Ничего
    Описание: Записывает значение в левую сторону списка.

    List.pushright (list , value)
    Аргументы: list - список, value - любое значение
    Возвращает: Ничего
    Описание: Записывает значение в правую сторону списка.

    List.popleft (list)
    Аргументы: list - список
    Возвращает: Крайнее слева значение списка
    Описание: Выдает последнее значение с левой стороны списка, после чего оно удаляется из списка. Если список пуст, то выдает nil.

    List.popright (list)
    Аргументы: list - список
    Возвращает: Крайнее справа значение списка
    Описание: Выдает последнее значение с правой стороны списка, после чего оно удаляется из списка. Если список пуст, то выдает nil.

    List.clear (list)
    Аргументы: list - список
    Возвращает: Ничего
    Описание: Очищает список.

    List.size (list)
    Аргументы: list - список
    Возвращает: 1 целое число
    Описание: Выдает количество значений в списке.


    P.S. - Корейцы сперли эти функций из книжки по Lua (http://www.lua.org/pil/11.4.html)[​IMG]

    Со списками мы ознакомились- самое время переходить дальше. А дальше уже начинается интересное- мы вплотную подошли к применению наших знаний на практике, а именно к изучению функций, которые созданы с использованием базовых функций, описанных в Const.lua. Данные функции занимаются вычислениями расстояния и производят некоторые логические операции на основе него. Перейдем же к их изучению:

    function GetDistance (x1,y1,x2,y2)
    return math.floor(math.sqrt((x1-x2)^2+(y1-y2)^2))
    end
    Аргументы: x1,y1 - координаты первой клетки x2,y2 - координаты второй клетки
    Возвращает: 1 целое число
    Описание: Выдает расстояние между 2-мя клетками. math.floor это функция из стандартной математической библиотеки, которая берет только целую часть числа, так как в игре расстояние не бывает дробным- оно измеряется в целых клетках. math.sqrt также функция математической библиотеки, которая берет квадратный корень из данной ей числа. В нашем случае это корень из суммы квадратов разности координат(школьное выражение, для вычисления длинны отрезка по его координатам). На самом деле, эта функция выдает не совсем точный результат, ибо при клеточным измерении в диагонали, и в большей из ее проекции на оси координат одинаковое количество клеток. Короче говоря, получается такой прямоугольный треугольник, где диагональ всегда равна большему катету(если измерять по клеткам), следовательно традиционные формулы алгебры тут не применимы. Чтобы вы не гадали как же испавить это, я дам вам готовый код замещения содержимого функции:
    return math.max(math.abs(x1-x2),math.abs(y1-y2))
    function GetDistance2 (id1, id2)
    local x1, y1 = GetV (V_POSITION,id1) -- Запрашиваем позицию первого объекта
    local x2, y2 = GetV (V_POSITION,id2) -- Запрашиваем позицию второго объекта

    if (x1 == -1 or x2 == -1) then -- Если хотя бы один из них вне зоны видимости, то ...
    return -1 -- ... нам ничего не остается делать как выдать знак об этом(здесь решили, что этим знаком будет -1)


    end

    return GetDistance (x1,y1,x2,y2) -- Вычисляем расстояние по координатам и возвращаем его
    end
    Аргументы: id1 - ID первого объекта, id2 - ID второго объекта
    Возвращает: 1 целое число
    Описание: Выдает расстояние между 2-мя объектами. Мы считываем координаты объектов и смотрим, если вдруг один из них вне зоны нашей видимости. В этом случае мы вычислить ничего не может и выдадим -1, чтобы сообщить об этом. Если же все впорядке, то мы передаем координаты нашей прошлой функции, которая вычислит для нас расстояние, и уже данная функция выдаст этот результат.
    function GetOwnerPosition (id)
    return GetV (V_POSITION,GetV(V_OWNER,id))
    end
    Аргументы: id - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: Простейшая функция, которая выдает координаты вашего алхимика. Сначало GetV(V_OWNER,id) выдает ID хозяина, после чего оно становится аргументом для еще одной функции GetV, которая выдает координаты алхимика.
    function GetDistanceFromOwner (id)
    local x1, y1 = GetOwnerPosition (id) -- Запрашиваем позицию хозяина
    local x2, y2 = GetV (V_POSITION,id) -- Запрашиваем позицию объекта

    if (x1 == -1 or x2 == -1) then -- Если объект вне зоны видимости(глупо проверять хозяина, ибо он всегда в центре экрана), то ...
    return -1 -- ... делать нечего- выдаем -1
    end

    return GetDistance (x1,y1,x2,y2) -- Вычисляем расстояние по координатам и возвращаем его
    end
    Аргументы: id - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: Структура идентична функции GetDistance2, только теперь мы вычисляем не расстояние между любыми объектами, а конкретно между алхимиком и гомункулом.
    function IsOutOfSight (id1,id2)
    local x1,y1 = GetV (V_POSITION,id1) -- Запрашиваем позицию первого объекта
    local x2,y2 = GetV (V_POSITION,id2) -- Запрашиваем позицию второго объекта

    if (x1 == -1 or x2 == -1) then -- Если хотя бы один из них вне зоны нашей видимости, то ...
    return true -- ... мы решаем, что они вне зоны видимости друг друга(а вот и не правда! вдруг один из них стоит на 17 клеток от нас(вне нашей зоны), а другой на 10(в нашей зоне), тогда они видят друг друга, вообщем, данная функция не всегда дает верный результа
    end

    local d = GetDistance (x1,y1,x2,y2) -- Итак, они в зоне нашей видимости и мы вычисляем рассмояние между ними

    if d > 20 then -- Если расстояние больше 20-ти клеток(на нашем сервере нужно поставить 14), то ...
    return true -- ... объекты вне видимости друг друга
    else -- В противном случае ...
    return false -- ... они видят друг друга
    end
    end
    Аргументы: id1 - ID первого объекта, id2 - ID второго объекта
    Возвращает: true или false
    Описание: Первая функция, где мы сталкиваемся с возвращением true/false значений. Как видно, название содержит в себе утверждение IsOutOfSight(вне зоны видимости), что является как бы негласным правилом, так как данные функции создаются для проверок с помощью if, например:
    id1 = 1
    id2 = 2

    if IsOutOfSight(id1,id2) then -- По-английски можно читать так: "Если вне зоны видимости id1 по-отношению к id2, то ...", вообщем, делается это только для повышения читаемости кода
    -- Что-то делаем
    end

    if not IsOutOfSight(id1,id2) then -- Тут можно использовать и not, тогда утверждение будет: "Если не вне зоны видимости id1 по-отношению к id2, то ..."
    end

    if IsOutOfSight(id1,id2) == true then -- При желании можно пользоваться и таким вариантом, но, мне кажется, что он значительно понижает читаемость
    end
    Итак, если утверждение IsOutOfSight правдиво, применительно к одному объекту относительно другого, то функция выдаст true, если же нет, то false. Данная функция разрабатывалась в основном для определения того есть ли тот или иной объект в зоне нашей видимости, тоесть один из id это наш алхимик или гомункул. Если GetV выдает -1, значит в зоне видимости объекта нет, и функция выдаст true. Если GetV не выдало -1, значит объект в зоне нашей видимости, так как максимальный обзор на нашем сервере 14 клеток, следовательно значение d будет всегда меньше, чем 20. Однако в случае, если эта функция будет вычислясь расстояние между другими двумя объектами, то оно вполне может составить 20 клеток и более, что будет означать, что объекты вне зоны видимости друг друга. Здесь надо оговорить, что на нашем сервере максимальный обзор всего 14 клеток, поэтому в данной функции уместно заменить 20 на 14, чтобы она работала корректнее. Также, при вычислении расстояния между объектами вне зоны нашей видимости она всегда выдает true, что во многих случаях является неправильным результатом.
    function IsInAttackSight (id1,id2)
    local x1,y1 = GetV (V_POSITION,id1) -- Запрашиваем позицию гомункула
    local x2,y2 = GetV (V_POSITION,id2) -- Запрашиваем позицию объекта

    if (x1 == -1 or x2 == -1) then -- Если объект вне зоны нашей видимости(гомункул тоже проверяется, что также глупо как и проверять хозяина), то ...
    return false -- ... для гомункула он недосягаем
    end

    local d = GetDistance (x1,y1,x2,y2) -- Если же объект нам виден, то мы вычисляем расстояние до него и ...
    local a = 0 -- ... объявляем переменную для расстояния текущей атаки гомункула

    if (MySkill == 0) then -- Если мы не хотим использовать скилл, то ...
    a = GetV (V_ATTACKRANGE,id1) -- ... записываем расстояние простой атаки гомункула в нашу переменную
    else -- А если хотим, то ...
    a = GetV (V_SKILLATTACKRANGE,id1,MySkill) -- ... записываем расстояние атаки скиллом гомункула в нашу переменную
    end

    if a >= d then -- Если нам хватает расстояния для атаки, то ...
    return true -- ... мы так и отвечаем
    else -- А если нет, то ...
    return false -- ... отвечаем "нет"
    end
    end
    Аргументы: id1 - ID вашего гомункула, id2 - ID цели
    Возвращает: true или false
    Описание: В начале, как и в предыдущих примерах, идет проверка на нахождение объекта в зоне нашей видимости. Если такового объекта нет, то утверждение IsInAttackSight(на достаточном расстоянии для атаки) не подтверждается и функция выдаст false. Если же данное несчастье нас миновало, то мы вычисляем расстояние до объекта, а также объявляем одну переменную, в которую потом запишем нужное для атаки расстояние. Но, чтобы решить какое расстояние нам нужно в данный момент, мы проверяем глобальную переменную MySkill, с которой мы познакомимся позже, а сейчас вам достаточно лишь знать, что, если она равна нулю, то гомункул не планирует использовать скилл(Caprise, Moonlight или S. B. R. 44) по-отношению к цели, поэтому расстояние атаки равно дальности обычного укуса гомункула, тоесть одной клетке. Однако, если гомункул хочет ударить скиллом, то расстояние атаки берется по дальности удара скилла. А конце идет проверка- если расстояние атаки равно или меньше, чем расстоящие до цели, то наше утверждение верно, и функция выдаст true.


    2 файла позади, остался последний и самый главный. В нем покоются "мозги" нашего гомункула.

    ---

    VII. Разбор скрипта AI

    AI.lua - Подробное изучение скрипта AI гомункула, полное практическое применение всех полученных ранее знаний:

    Собственно, открываем наш файл AI.lua. С первых строк мы видим уже знакомую картину:
    require "./AI/Const.lua"
    require "./AI/Util.lua"
    Оставим ее без комментариев и опустимся чуть ниже. А ниже у нас снова именные константы, которые обозначают состояния(см. ГОМУНКУЛЫ :: Полное Руководство -> Общие сведения -> AI) гомункула, в которых он может находиться:

    ВНИМАНИЕ!
    Большая часть состояний сделана "с запасом". Многие из них относятся к несуществующим пока командам, а многие вообще не нужны в контексте стандартного AI, поэтому я прокомментирую только нужную нам часть.
    Также хочу отметить, что эти переменные получают значение вне главной функции(которую мы рассмотрим сразу после них), следовательно они их получают только один раз, до первого запуска этой функции.


    0|IDLE_ST - Основное состояние покоя
    1|FOLLOW_ST - Состояние следования за хозяином
    2|CHASE_ST - Состояние преследования врага
    3|ATTACK_ST - Состояния атаки врага
    4|MOVE_CMD_ST - Состояние перехода на указанную клетку
    12|FOLLOW_CMD_ST - Пассивное состояние

    Сразу после состояний идут глобальные переменные(также комментирую только востребованные):

    MyState = IDLE_ST - Состояние гомункула(при респавне(см. ГОМУНКУЛЫ :: Полное Руководство -> Общие сведения -> AI) гомункула, его состояние будет IDLE)
    MyEnemy = 0 - Цель гомункула
    MyDestX = 0 - x-координата назначения гомункула
    MyDestY = 0 - y-координата назначения гомункула
    ResCmdList = List.new() - Список для хранения команд, посылаемых через Alt + Shift + Click
    MyID = 0 - ID вашего гомункула
    MySkill = 0 - Скилл атаки гомункула
    MySkillLevel = 0 - Уровень этого скилла

    Далее мы начинаем по-очередный разбор функций самого AI, которые являются сердцем нашего скрипта.
    Начнем мы с функции под названием AI(которая находится в самом конце скрипта), так как она является главной(вызывается первой при обработке скрипта). Данная функция постоянно запускается вашим клиентом(со средней частотой- несколько раз в секунду), пока ваш гомункул находится в поле вашей видимости, поэтому все действия в ней повторяются как бы в бесконечном цикле. Помните также, что при перезагрузке скрипта(респавне гомункула) все данные в переменных будут потеряны и работа скрипта начнется "с чистого листа", поэтому, если вы, например, хотите, чтобы гомункул, не терял из памяти свою цель после смерти, то нужно записывать ее в файл, а затем считывать ее оттуда при загрузке скрипта.
    function AI(myid)

    MyID = myid -- Заносим ID гомункула в глобальную переменную
    local msg = GetMsg (myid) -- Считываем таблицу команды, посланной через Alt
    local rmsg = GetResMsg (myid) -- Считываем таблицу команды, посланной через Alt + Shift

    if msg[1] == NONE_CMD then -- Если первичная команда не поступила, то ...
    if rmsg[1] ~= NONE_CMD then -- ... проверяем поступила ли вторичная команда, и, если это так, то ...
    if List.size(ResCmdList) < 10 then -- ... проверяем длинну списка действий, и, если в нем еще есть место, то ...
    List.pushright (ResCmdList,rmsg) -- ... заносим туда нашу команду
    end
    end
    else -- А если же она всетаки поступила, то ...
    List.clear (ResCmdList) -- ... очищаемсписокпоследовательныхдействийи ...
    ProcessCommand (msg) -- ... передаемтаблицукомандыдалеедляобработки
    end

    -- Выясняемфункциюкакогосостояниянамзагружать:
    if (MyState == IDLE_ST) then
    OnIDLE_ST ()
    elseif (MyState == CHASE_ST) then
    OnCHASE_ST ()
    elseif (MyState == ATTACK_ST) then
    OnATTACK_ST ()
    elseif (MyState == FOLLOW_ST) then
    OnFOLLOW_ST ()
    elseif (MyState == MOVE_CMD_ST) then
    OnMOVE_CMD_ST ()
    elseif (MyState == STOP_CMD_ST) then
    OnSTOP_CMD_ST ()
    elseif (MyState == ATTACK_OBJECT_CMD_ST) then
    OnATTACK_OBJECT_CMD_ST ()
    elseif (MyState == ATTACK_AREA_CMD_ST) then
    OnATTACK_AREA_CMD_ST ()
    elseif (MyState == PATROL_CMD_ST) then
    OnPATROL_CMD_ST ()
    elseif (MyState == HOLD_CMD_ST) then
    OnHOLD_CMD_ST ()
    elseif (MyState == SKILL_OBJECT_CMD_ST) then
    OnSKILL_OBJECT_CMD_ST ()
    elseif (MyState == SKILL_AREA_CMD_ST) then
    OnSKILL_AREA_CMD_ST ()
    elseif (MyState == FOLLOW_CMD_ST) then
    OnFOLLOW_CMD_ST ()
    end
    endАргументы: myid - ID вашегогомункулаВозвращает: НичегоОписание: Главнаяфункция, которойаргументомпередается ID вашегогомункула. Как известно, аргумент функции локальная переменная, поэтому ее сразу записывают в глобальную(чтобы ее можно было использовать в других частях скрипта). Затем идет считывание таблиц текущих команд от игрока и запись их в локальные переменные msg и rmsg. Далее идет проверка на то, поступила ли команда от нажатия Alt + Click/T(тоесть от функции GetMsg). Проверка этих команд идет первой, так как в стандартном AI эти команды имеют высший приоритет по-отношению к остальным. Если она не поступила, то идет проверка поступила ли команда от Alt + Shift + Click(тоесть от функции GetResMsg) и в случае, если это так, то мы заносим ее в список последовательных действий(который затем обрабатывается в состоянии покоя), предварительно проверив не привышает ли он 10 записей, ибо больше команд разработчики этого AI почему-то хранить не хотят. Это было все, что относится к случаю, когда команда от GetMsg не поступила, в противном же случае мы очищаем наш список(чтобы не хранить в памяти старые команды) и передаем командную таблицу функции ProcessCommand(с ней мы познакомимся сразу после этой функции), которая далее идентифицирует команду и передает ее дальше для исполнения. В самом низу мы видим, так называемую, "лестницу" elseif, с помощью которой решается функцию какого состояния запустить, исходя из текущего состояния MyState.


    Как и было обещано- далее мы рассмотрим функцию обработки комманд, которая начинается на 207 строке скрипта:
    function ProcessCommand (msg)

    -- Выясняем какая это команда и, исходя из этого, запускаем соответствующую функцию(и передаем ей сопутствующие значения, если такие имеются):
    if (msg[1] == MOVE_CMD) then
    OnMOVE_CMD (msg[2],msg[3])
    TraceAI ("MOVE_CMD")
    elseif (msg[1] == STOP_CMD) then
    OnSTOP_CMD ()
    TraceAI ("STOP_CMD")
    elseif (msg[1] == ATTACK_OBJECT_CMD) then
    OnATTACK_OBJECT_CMD (msg[2])
    TraceAI ("ATTACK_OBJECT_CMD")
    elseif (msg[1] == ATTACK_AREA_CMD) then
    OnATTACK_AREA_CMD (msg[2],msg[3])
    TraceAI ("ATTACK_AREA_CMD")
    elseif (msg[1] == PATROL_CMD) then
    OnPATROL_CMD (msg[2],msg[3])
    TraceAI ("PATROL_CMD")
    elseif (msg[1] == HOLD_CMD) then
    OnHOLD_CMD ()
    TraceAI ("HOLD_CMD")
    elseif (msg[1] == SKILL_OBJECT_CMD) then
    OnSKILL_OBJECT_CMD (msg[2],msg[3],msg[4],msg[5]) -- Здесь ошибочка- четвертый аргумент не нужен
    TraceAI ("SKILL_OBJECT_CMD")
    elseif (msg[1] == SKILL_AREA_CMD) then
    OnSKILL_AREA_CMD (msg[2],msg[3],msg[4],msg[5])
    TraceAI ("SKILL_AREA_CMD")
    elseif (msg[1] == FOLLOW_CMD) then
    OnFOLLOW_CMD ()
    TraceAI ("FOLLOW_CMD")
    end
    end
    Аргументы: msg - таблица команды
    Возвращает: Ничего
    Описание: Перед нами очередная конструкция из elseif, которая, как и в случае с состояниями, решает какую функцию запустить, исходя из данной команды. Как видно, сопутствующие команде значения(если такие имеются), находящиеся под последующими после единицы(2,3 и тд) ключами, передаются далее функции, которая занимается уже конкретным исполнением этой команды. Завершает каждый блок функция TraceAI, которая записывает(если /traceai включен) лог о том, что поступила та или иная команда.


    Перейдем к по-очередному разбору функций для исполнения команд.

    ВНИМАНИЕ!
    Опять же, я не буду комментировать функции для исполнения несуществующих команд. Такие функции можно смело удалять из скрипта, вместе с сопутствующими им состояниями, так как они никоим образом не задействованы в работе AI.



    51 строчка- функция для исполнения команды MOVE_CMD:
    function OnMOVE_CMD (x,y)

    TraceAI ("OnMOVE_CMD") -- Говорим, что мы исполняем эту команду

    if (x == MyDestX and y == MyDestY and MOTION_MOVE == GetV ( V _ MOTION ,MyID)) then -- Если мы уже идем на эту клетку, то ...
    return -- ... нам нет смысла прогонять эту функцию и мы сразу ее прекращаем
    end

    local curX, curY = GetV (V_POSITION,MyID) -- Запрашиваем позицию гомункула

    if (math.abs(x-curX)+math.abs(y-curY) > 15) then -- Если расстояние до клетки назначения больше, чем 15 клеток(гомункул не может идти так далеко), то ...
    List.pushleft (ResCmdList,{MOVE_CMD,x,y}) -- ... мы записываем ее в список действий для исполнения в будущем и ...
    x = math.floor((x+curX)/2) -- ...
    y = math.floor((y+curY)/2) -- ... берем вместо нее в 2 раза более близкую клетку
    end

    Move (MyID,x,y) -- Переходим на нашу клетку

    MyState = MOVE_CMD_ST -- Меняем состояние для контроля перехода на клетку и ...
    MyDestX = x -- ...
    MyDestY = y -- ... записываем, что мы идем на эту клетку, а также ...
    MyEnemy = 0 -- ... стираем цель и ...
    MySkill = 0 -- ... скилл тоже
    end
    Аргументы: x,y - координаты клетки
    Возвращает: Ничего
    Описание: Первым делом идет вполне бесполезная проверка на то, если гомункул уже идет на данную клетку. В этом случае фукнция сразу прекращает свою работу, чтобы не дублировать текущее действие. Далее мы видим довольно хитрую конструкцию, которая играет весьма важную роль. Итак, мы берем текущее месторасположение нашего гомункула и проверяем, если та клетка, на которую мы приказали ему идти находится на большем расстоянии от него, чем 15 клеток(гомункул не может идти на клетку, которая дальше от него, чем обзор экрана), то мы записываем ее в список ResCmdList(вместе с командой MOVE_CMD) и заменяем клетку назначения на в 2 раза более близкую к гомункулу. Впоследствии, когда он достигнет этой более близкой клетки, его состояние станет IDLE_ST, и уже в этом состоянии та, изначальная клетка, считается из списка вместе с командой, после чего эта команда передастся в данную функцию и гомункул благополучно перейдет на ту клетку, что была на большем расстоянии, чем 15 клеток. Таким образом, этот кусок кода позволяет гомункулу переходить на большие расстояния, чем 15 клеток. А в финальной части идет сам приказ- перейти на указанную клетку. Затем его состояние меняют на MOVE_CMD_ST(состояние движения на указанную клетку), в глобальные переменные назначения записывают координаты клетки, а цель и текущий скилл атаки(если такие имелись) стирают.





    371 строчка- функция для контроля исполнения команды MOVE_CMD:
    function OnMOVE_CMD_ST ()

    TraceAI ("OnMOVE_CMD_ST") -- Говорим, что мы в этом состоянии

    local x, y = GetV (V_POSITION,MyID) -- Запрашиваем координаты гомункула

    if (x == MyDestX and y == MyDestY) then -- Если мы уже дошли до клетки назначения, то ...
    MyState = IDLE_ST -- ... нам в этом состоянии больше делать нечего
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Данная функция выполняется до тех пор, пока гомункул не достигнет клетки, на которую ему приказали идти. После достижения этой клетки состояние меняется на IDLE_ST.


    97 строчка- функция для исполнения команды ATTACK_OBJECT_CMD:
    function OnATTACK_OBJECT_CMD (id)

    TraceAI ("OnATTACK_OBJECT_CMD") -- Говорим, что мы исполняем эту команду

    MySkill = 0 -- На всякий случай стираем скилл и ...
    MyEnemy = id -- ... записываем нашу цель, а также ...
    MyState = CHASE_ST -- ... меняем состояние на погоню за целью
    end
    Аргументы: id - ID цели
    Возвращает: Ничего
    Описание: Простая функция, которая записывает данную ей цель и меняет состояние на CHASE_ST, а также, на всякий случай, стирает атакующий скилл, так как команда не предусматривает атаку скиллом.


    155 строчка- функция для исполнения команды SKILL_OBJECT_CMD:
    function OnSKILL_OBJECT_CMD (level,skill,id)

    TraceAI ("OnSKILL_OBJECT_CMD") -- Говорим, что мы исполняем эту команду

    MySkillLevel = level -- Записываем уровень скилла, ...
    MySkill = skill -- ... его ID, ...
    MyEnemy = id -- ... ID врага и ...
    MyState = CHASE_ST -- ... меняем состояние на погоню
    end
    Аргументы: level - уровень скилла , skill - ID скилла, id - ID цели
    Возвращает: Ничего
    Описание: Еще одна простая функция, которая тоже записывает данную ей цель и меняет состояние на CHASE_ST, но на этот раз не стирает, а записывает скилл и его уровень, так как команда- атаковать скиллом.


    185 строчка- функция для исполнения команды FOLLOW_CMD:
    function OnFOLLOW_CMD ()

    if (MyState ~= FOLLOW_CMD_ST) then -- Если мы не в пассивном состоянии, то ...
    MoveToOwner (MyID) -- ... говорим гомункулу подойти к нам, ...
    MyState = FOLLOW_CMD_ST -- ... меняем состояние на пассивное, ...
    MyDestX, MyDestY = GetV (V_POSITION,GetV(V_OWNER,MyID)) -- ... записываем, что мы идем к хозяину, а также ...
    MyEnemy = 0 -- ... стираем врага и ...
    MySkill = 0 -- ... скилл тоже
    TraceAI ("OnFOLLOW_CMD") -- Ну, и говорим, что мы исполняем эту команду
    else -- В противном случае ...
    MyState = IDLE_ST -- ... меняем состояние на покой и ...
    MyEnemy = 0 -- ... тоже стираем врага и ...
    MySkill = 0 -- ... скилл
    TraceAI ("FOLLOW_CMD_ST --> IDLE_ST") -- Но в этом случае говорим, что мы переходим в состояние покоя
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Вначале стоит проверка, которая решает в каком состоянии сейчас гомункул. Если сейчас не пассивное состояние, то гомункул переходит в него, вместе с обнулением врага и скилла атаки, а также ему дается команда подойти к хозяину, поэтому координаты хозяина записываются в переменные назначения. Ежели сейчас пассивное состояние, то гомункул переходит в основное состояние покоя, и обнуляются переменные врага и скилла.
    Arny и -=FaNaTiK=- нравится это.
  4. maas

    maas Игрок

    512 строчка- функция для контроля исполнения команды FOLLOW_CMD:
    function OnFOLLOW_CMD_ST ()

    TraceAI ("OnFOLLOW_CMD_ST") -- Говорим, что мы в этом состоянии

    local ownerX, ownerY, myX, myY
    ownerX, ownerY = GetV (V_POSITION, GetV (V_OWNER,MyID)) -- Запрашиваем позицию хозяина и ...
    myX, myY = GetV (V_POSITION,MyID) -- ... гомункула

    local d = GetDistance (ownerX,ownerY,myX,myY) -- Вычисляем расстояние между ними

    if (d <= 3) then -- Если оно больше или равно трем клеткам, то ...
    return -- ... ничего не делаем и выходим из функции
    end

    local motion = GetV ( V _ MOTION ,MyID) -- Запрашиваем действие гомункула

    if (motion == MOTION_MOVE) then -- Если гомункул движется, то ...
    d = GetDistance (ownerX, ownerY, MyDestX, MyDestY) -- ... вычисляем расстояние от хозяина до точки назначения ...

    if (d > 3) then -- Если точка назначения дальше, чем 3 клетки, то ...
    MoveToOwner (MyID) -- ... приказываем гомункулу подойти к хозяину ...
    MyDestX = ownerX -- ...
    MyDestY = ownerY -- ... записываем, что мы идем к нему и ...
    return -- ... выходим из функции(хотя это итак ее конец)
    end
    else -- В противном случае ...
    MoveToOwner (MyID) -- ... приказываем гомункулу подойти к хозяину и, а также ...
    MyDestX = ownerX -- ...
    MyDestY = ownerY -- ... записываем, что мы идем к нему и ...
    return -- ... выходим из функции(хотя это итак ее конец)
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Суть всего этого бессмысленного нагромождения кода можно свести к тому, что, если расстояние между гомункулом и алхимиком меньше или равно 3, то ничего не делается, а если оно больше, то гомункул идет к хозяину. Фактически проверка на MOTION тут совершенна ненужна(когда я ее убрал, гомункул стал намного резвее за мной ходить), также как и не нужны return-ы в конце функции(данная глупость повторяется и во многих последующих функциях).






    Далее мы рассмотрим функции основных состояний. А начнем мы с первостепенного IDLE_ST, функция которого начинается на 245 строке:



    function OnIDLE_ST ()

    TraceAI ("OnIDLE_ST") -- Говорим, что мы в этом состоянии

    local cmd = List.popleft (ResCmdList) -- Считываем первую команду из списка действий

    if (cmd ~= nil) then -- Если команда имеется, то ...
    ProcessCommand (cmd) -- ... передаем ее для обработки и ...
    return -- ... выходим из функции
    end

    local object = GetOwnerEnemy (MyID) -- Находим врага хозяина

    if (object ~= 0) then -- Если враг имеется, то ...
    MyState = CHASE_ST -- ... меняем состояние на погоню и ...
    MyEnemy = object -- ... записываем его ...
    TraceAI ("IDLE_ST -> CHASE_ST : MYOWNER_ATTACKED_IN") -- ... а также говорим, что мы перешли в состояние погони из-за врага хозяина и ...
    return -- ... выходим из функции
    end

    object = GetMyEnemy (MyID) -- Находим врага гомункула

    if (object ~= 0) then -- Если враг имеется, то ...
    MyState = CHASE_ST -- ... меняем состояние на погоню и ...
    MyEnemy = object -- ... записываем его ...
    TraceAI ("IDLE_ST -> CHASE_ST : ATTACKED_IN") -- ... а также говорим, что мы перешли в состояние погони из-за врага гомункула и ...
    return -- ... выходим из функции
    end

    local distance = GetDistanceFromOwner(MyID) -- Находим расстояние между гомункулом и хозяином

    if (distance > 3 or distance == -1) then -- Если оно превышает 3 клетки(второе условие глупо, ибо за пределами экрана скрипт не пашит), то ...
    MyState = FOLLOW_ST -- ... переходим в состояние следования за хозяином и ...
    TraceAI ("IDLE_ST -> FOLLOW_ST") -- ... говорим, что мы в него перешли ...
    return -- ... ну, и выходим из функции(хотя, опять же, зачем, когда это итак ее конец)
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Все команды, которые заносятся в список ResCmdList начинают обрабатываться только в этом состоянии. И если в данный момент команда имеется, то скрипт сразу переходит к ее исполнению. Минуя данное событие, мы опускаемся ниже, где видим конструкцию, которая проверяет есть ли в данный момент у хозяина враг, путем обращения к функции GetOwnerEnemy(с ней мы ознакомимся чуть ниже). При наличии такового, мы записываем его в переменную врага и меняем состояние на погоню за врагом. Имеется также и вторая такая конструкция, которая проверяет уже врага гомункула. И в самом конце, если врагов мы так и не нашли, то мы проверяем расстояние до хозяина, и если оно больше 3-х или гомункул находится за пределами видимости(глупое условие, так как за пределами видимости мы не можем дать ему команду подойти к хозяину), то мы меняем состояние на следование за хозяином.



    551 строчка- функция для определения ближайшего врага алхимика:
    function GetOwnerEnemy (myid)

    local result = 0 -- Переменная для конечного результата
    local owner = GetV (V_OWNER,myid) -- ID хозяина
    local actors = GetActors () -- Таблица окружающих объектов
    local enemys = {} -- Таблица для нужных нам объектов
    local index = 1 -- Вспомогательная переменная для индексирования таблицы enemys
    local target -- Вспомогательная переменная для временного хранения цели объекта

    for i,v in ipairs(actors) do -- Запускаем цикл через все содержимое таблицы actors
    if (v ~= owner and v ~= myid) then -- Если текущий объект не гомункул и не хозяин, то ...
    target = GetV (V_TARGET,v) -- ... мы запрашиваем его цель

    if (target == owner) then -- Если его цель- хозяин, то ...
    if (IsMonster(v) == 1) then -- ... мы проверяем, если это монстр, и, если это так, то ...
    enemys[index] = v -- ... это наш клиент, которого мы заносим в нашу таблицу, после чего ...
    index = index+1 -- ... готовим индекс для следующего клиента
    else -- В противном случае(если он не монстр) ...
    local motion = GetV ( V _ MOTION ,i) -- ... мы смотрим действие объекта(здесь ошибка- вместо i надо v)

    if (motion == MOTION_ATTACK or motion == MOTION_ATTACK2) then -- Если его действие- атака(тоесть атака хозяина раз он является целью объекта), то ...
    enemys[index] = v -- ... это тоже наш клиент и ...
    index = index+1 -- ... мы также готовим индекс для следующего
    end
    end
    end
    end
    end

    -- Итак, у нас имеются отсеянные злюки, настроенные агрессивно по-отношению к хозяину, теперь нам нужно решить кого из них бить- возьмем самого близкого
    local min_dis = 100 -- Объявляем переменную для текущего минимального расстояния и заносим туда какое-нибудь большое число
    local dis -- Объявляем вспомогательную переменную для текущего расстояния

    for i,v in ipairs(enemys) do -- Запускаем цикл через все содержимое таблицы enemys
    dis = GetDistance2 (myid,v) -- Вычисляем расстояние от гомункула до текущего объекта

    if (dis < min_dis) then -- Если оно меньше, чем найденное на данный момент минимальное расстояние, то ...
    result = v -- ... на данный момент это лучшая для нас цель и ...
    min_dis = dis -- ... данное расстояние становится минимальным
    end
    end

    return result -- Ближайщий злюка найден и мы его выдаем(а если таких нету, то будет выдан 0)
    end
    Аргументы: myid - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: Вот то самое место, где хорошо бы вспомнить вид цикла for с функцией итерации. Я обещал, что покажу как он действует, но не стану вдаваться в подробности. Так вот, если вкратце, то данный тип цикла каждый раз "достает" из таблицы значение с помощью специальной функции итерации. В нашем случае это таблица окружающих объектов, которую мы получаем при вызове GetActors, а функцию итерации мы получаем, передав эту таблицу функции ipairs из стандартной библиотеки. ipairs может работать только с таблицами, в которых данные находятся по-очередно под числовыми ключами, начиная с 1-го, а наша таблица как-раз соответствует этим требованиям. При каждом новом "витке" цикла следующий элемент в таблице и его ключ считываются и записываются в локальные для цикла переменные(в нашем случае это i для ключа и v для значения), тоесть эти переменные каждый раз получают новые значения. Цикл продолжается, пока функция итерации не выдаст nil, тоесть отсутствие следующего значения.
    Итак, техническую сторону мы знаем, теперь разберем что же происходит в этом куске кода. Первым делом, идет объявление локальных переменных. Далее идет цикл, который отбирает нужных для нас объектов, а именно- монстров, нацеленных на хозяина. Вначале проверяется, если объект не наш гомункул и не его хозяин. Затем нацелен ли объект на хозяина, и наконец, если объект это монстр, то это наша цель, а если это не монстр(видимо здесь имеется ввиду игрок), то мы проверяем атакует ли он в данный момент хозяина(тут допущена ошибка- нас интересует действие объекта, а не его порядкового номера, можете смело исправить i на v), и, если сие деяние имеет место, то данный объект тоже нам подходит. Таким образом, проверяется каждый объект и формируется таблица enemys, которая имеет туже структуру(все значения находятся под числовыми ключами начиная с 1-го), однако в enemys находятся уже только интересующие нас объекты. После сортировки нам важно знать какая из целей(если их много) ближе всего к нам. Для этого мы запускаем новый цикл, используя таблицу наших найденных злюк enemys. А до этого, объявляем 2 переменные, одну для хранения текущего расстояния, другую для хранения минимального расстояния(в эту переменную надо занести какое-нибудь число, начиная с 30, так как это максимальное расстояние между 2-мя клетками на экране). Ну, а в самом цикле идет проверка: если рассторяние до текущей цели меньше, чем минимальное расстояние, которое нашли до него, то минимальным расстоянием становится текущее расстояние, а сама цель записывается в переменную result, которая выдается в конце функции. Таким образом, мы нашли ближайщую цель, которая в данный момент нацелена на или уже атакует хозяина, а если таких целей нету, то функция выдаст 0(первичное значение result).


    593 строчка- функция для выбора пассивного/агрессивного метода определения врага гомункула:
    function GetMyEnemy (myid)
    local result = 0 -- Переменная для конечного результата
    local type = GetV (V_HOMUNTYPE,myid) -- Запрашиваем тип гомункула

    if (type == LIF or type == LIF_H or type == AMISTR or type == AMISTR_H or type == LIF2 or type == LIF_H2 or type == AMISTR2 or type == AMISTR_H2) then -- Если у нас Амистр или Лиф, то ...
    result = GetMyEnemyA (myid) -- ... находим врага по пассивному методу
    elseif (type == FILIR or type == FILIR_H or type == VANILMIRTH or type == VANILMIRTH_H or type == FILIR2 or type == FILIR_H2 or type == VANILMIRTH2 or type == VANILMIRTH_H2) then -- А если Филлир или Ванилмиртх, то ...
    result = GetMyEnemyB (myid) -- ... по агрессивному
    end

    return result -- И в конце выдаем результат
    end
    Аргументы: myid - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: Функция представляет из себя выбор поведения гомункула взависемости от его вида. Все формы ванилмиртха и филлира агрессивны по стандартному AI, а лиф и амистр пассивны. Что означают эти 2 модели поведения рассмотрим ниже.


    611 строчка- функция для пассивного метода определения ближайшего врага гомункула:
    function GetMyEnemyA (myid)

    local result = 0 -- Переменная для конечного результата
    local owner = GetV (V_OWNER,myid) -- ID хозяина
    local actors = GetActors () -- Таблица окружающих объектов
    local enemys = {} -- Таблица для нужных нам объектов
    local index = 1 -- Вспомогательная переменная для индексирования таблицы enemys
    local target -- Вспомогательная переменная для временного хранения цели объекта

    for i,v in ipairs(actors) do -- Запускаем цикл через все содержимое таблицы actors
    if (v ~= owner and v ~= myid) then -- Если текущий объект не гомункул и не хозяин, то ...
    target = GetV (V_TARGET,v) -- ... мы запрашиваем его цель

    if (target == myid) then -- Если его цель- гомункул, то ...
    enemys[index] = v -- ... это наш клиент, которого мы заносим в нашу таблицу, после чего ...
    index = index+1 -- ... готовим индекс для следующего клиента
    end
    end
    end

    -- Итак, у нас имеются отсеянные злюки, настроенные агрессивно по-отношению к хозяину, теперь нам нужно решить кого из них бить- возьмем самого близкого
    local min_dis = 100 -- Объявляем переменную для текущего минимального расстояния и заносим туда какое-нибудь большое число
    local dis -- Объявляем вспомогательную переменную для текущего расстояния

    for i,v in ipairs(enemys) do -- Запускаем цикл через все содержимое таблицы enemys
    dis = GetDistance2 (myid,v) -- Вычисляем расстояние от гомункула до текущего объекта

    if (dis < min_dis) then -- Если оно меньше, чем найденное на данный момент минимальное расстояние, то ...
    result = v -- ... на данный момент это лучшая для нас цель и ...
    min_dis = dis -- ... данное расстояние становится минимальным
    end
    end

    return result -- Ближайщий злюка найден и мы его выдаем(а если таких нету, то будет выдан 0)
    end
    Аргументы: myid - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: См. функцию GetOwnerEnemy. Здесь использован тотже принцип, только в намного более логичной форме. Тоесть проверка на то, является ли атакер монстром или игроком отсутствует, что в принципе логично, ибо любой атакер это либо игрок, либо монстр(НПС у нас вроде как пока мирные[​IMG])


    648 строчка- функция для активного метода определения ближайшего врага гомункула:
    function GetMyEnemyB (myid)

    local result = 0 -- Переменная для конечного результата
    local owner = GetV (V_OWNER,myid) -- ID хозяина
    local actors = GetActors () -- Таблица окружающих объектов
    local enemys = {} -- Таблица для нужных нам объектов
    local index = 1 -- Вспомогательная переменная для индексирования таблицы enemys
    local type -- Непонятно для чего это

    for i,v in ipairs(actors) do -- Запускаем цикл через все содержимое таблицы actors
    if (v ~= owner and v ~= myid) then -- Если текущий объект не гомункул и не хозяин, то ...
    if (1 == IsMonster(v)) then -- ... мы смотрим, если он монстр и, если это так, то ...
    enemys[index] = v -- ... это наш клиент, которого мы заносим в нашу таблицу, после чего ...
    index = index+1 -- ... готовим индекс для следующего клиента
    end
    end
    end

    -- Итак, у нас имеются отсеянные злюки, настроенные агрессивно по-отношению к хозяину, теперь нам нужно решить кого из них бить- возьмем самого близкого
    local min_dis = 100 -- Объявляем переменную для текущего минимального расстояния и заносим туда какое-нибудь большое число
    local dis -- Объявляем вспомогательную переменную для текущего расстояния

    for i,v in ipairs(enemys) do -- Запускаем цикл через все содержимое таблицы enemys
    dis = GetDistance2 (myid,v) -- Вычисляем расстояние от гомункула до текущего объекта

    if (dis < min_dis) then -- Если оно меньше, чем найденное на данный момент минимальное расстояние, то ...
    result = v -- ... на данный момент это лучшая для нас цель и ...
    min_dis = dis -- ... данное расстояние становится минимальным
    end
    end

    return result -- Ближайщий злюка найден и мы его выдаем(а если таких нету, то будет выдан 0)
    end
    Аргументы: myid - ID вашего гомункула
    Возвращает: 1 целое число
    Описание: Тоже самое, что и в прошлой функции, только, так как это агрессивый метод определения врагов, то гомункул считает врагом любого монстра и сразу же идет в атаку.



    283 строчка- функция состояния FOLLOW_ST:
    function OnFOLLOW_ST ()

    TraceAI ("OnFOLLOW_ST") -- Говорим, что мы в данном состоянии

    if (GetDistanceFromOwner(MyID) <= 3) then -- Если расстояние между гомункулом и хозяином меньше или равно трем клеткам, то ...
    MyState = IDLE_ST -- ... меняем состояние на покой, ...
    TraceAI ("FOLLOW_ST -> IDLW_ST") -- ... говорим об этом(ошибка- не IDLW_ST а IDLE_ST) и ...
    return -- ... выходим из функции(хотя это итак ее конец)
    elseif ( GetV ( V _ MOTION ,MyID) == MOTION_STAND) then -- В противном случае, если гомункул стоит, то ...
    MoveToOwner (MyID) -- ... мы подзываем его к себе, ...
    TraceAI ("FOLLOW_ST -> FOLLOW_ST") -- ... говорим, что остаемся в этом же состоянии и ...
    return -- ... выходим из функции(хотя это итак ее конец)
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Крайне простая функция, которую можно было бы даже не выделять в отдельное состояние, а запихнуть в функцию IDLE_ST. По-сути это простой вариант функции для обработки пассивного состояния FOLLOW_CMD_ST- если гомункул находится на достаточно близком расстоянии от хозяина, то мы возвращаемся в состояние покоя, а если нет и если гомункул стоит(бесполезная проверка, ее смело можно стирать, без нее гомункул намного быстрее ходит за вами), то ему приказывается идти к хозяину.


    302 строчка- функция состояния CHASE_ST:
    function OnCHASE_ST ()

    TraceAI ("OnCHASE_ST") -- Говорим, что мы в этом состоянии

    if (true == IsOutOfSight(MyID,MyEnemy)) then -- Если мы потеряли цель из виду, то ...
    MyState = IDLE_ST -- ... переходим обратно в состояние покоя, ...
    MyEnemy = 0 -- ... стрираем цель, ...
    MyDestX, MyDestY = 0,0 -- ... пункт назначения, а также ...
    TraceAI ("CHASE_ST -> IDLE_ST : ENEMY_OUTSIGHT_IN") -- ... говорим, что мы потеряли цель и, поэтому перешли в другое состояние, и затем ...
    return -- ... выходим из функции
    end

    if (true == IsInAttackSight(MyID,MyEnemy)) then -- Если цель в на достаточном расстоянии для атаки, то ...
    MyState = ATTACK_ST -- ... переходим в состояние атаки, ...
    TraceAI ("CHASE_ST -> ATTACK_ST : ENEMY_INATTACKSIGHT_IN") -- ... говорим, что мы перешли в это состояние, так как можно атаковать цель и ...
    return -- ... выходим из функции
    end

    local x, y = GetV (V_POSITION,MyEnemy) -- Считываем позицию врага

    if (MyDestX ~= x or MyDestY ~= y) then -- Если он уже на другой клетке, то ...
    MyDestX, MyDestY = GetV (V_POSITION,MyEnemy) -- ... записываем координаты этой клетки в качестве нового пункта назначения и ...
    Move (MyID,MyDestX,MyDestY) -- ... идем туда, а также ...
    TraceAI ("CHASE_ST -> CHASE_ST : DESTCHANGED_IN") -- ... говорим, что мы остаемся в этом состоянии для того, так как враг перешел на другую клетку, и мы его еще не догнали, и затем ...
    return -- ... выходим из функции(хотя это итак ее конец)
    end
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Если гомункул находится в этом состоянии, значит ему дали приказ идти к врагу для атаки. При выходе цели из зоны видимости гомункула, идет возврат в состояние покоя, с сопутствующими ему событию обнулениями переменных. Если, наоборот, цель находится на достаточном расстоянии для атаки, то гомункул переходит в состояние атаки. Остался промежуточный вариант- когда цель в зоне нашей видимости, но пока еще далека, чтобы ее атаковать. В этом случае мы приказываем гомункулу идти к цели, проверяя перед этим не приказали ли мы уже идти гомункулу туда же.


    332 строчка- функция состояния ATTACK_ST:
    function OnATTACK_ST ()

    TraceAI ("OnATTACK_ST") -- Говорим, что мы в этом состоянии

    if (true == IsOutOfSight(MyID,MyEnemy)) then -- Если мы потеряли врага из виду, то ...
    MyState = IDLE_ST -- ... переходим в состояние покоя, ...TraceAI ("ATTACK_ST -> IDLE_ST") -- ... говоримобэтоми ...
    return -- ... выходимизфункции
    end

    if (MOTION_DEAD == GetV ( V _ MOTION ,MyEnemy)) then -- Если же враг умер, то мы также ...
    MyState = IDLE_ST -- ... переходим в состояние покоя, ...
    TraceAI ("ATTACK_ST -> IDLE_ST") -- ... говорим об этом и ...
    return -- ... выходим из функции
    end

    if (false == IsInAttackSight(MyID,MyEnemy)) then -- Если враг вышел из зоны нашего удара, то ...
    MyState = CHASE_ST -- ... мы снова гонимся за ним, ...
    MyDestX, MyDestY = GetV (V_POSITION,MyEnemy) -- ... записываем его координаты в переменные назначения и ...
    Move (MyID,MyDestX,MyDestY) -- ... идем к нему, а также ...
    TraceAI ("ATTACK_ST -> CHASE_ST : ENEMY_OUTATTACKSIGHT_IN") -- ... говорим, что мы перешли в состояние погони из-за того, что враг вне зоны удара, и затем ...
    return -- ... выходим из функции
    end

    -- Итак, враг в зоне нашего удара, и нам осталось проверить последнюю вещь:
    if (MySkill == 0) then -- Если мы не хотим атаковать скиллом, то ...
    Attack (MyID,MyEnemy) -- ... атакуем простым ударом
    else -- В противном случае ...
    SkillObject (MyID,MySkillLevel,MySkill,MyEnemy) -- Атакуем скиллом и ...
    MySkill = 0 -- ... обнуляем переменную, чтобы больше его не использовать
    end

    TraceAI ("ATTACK_ST -> ATTACK_ST : ENERGY_RECHARGED_IN") -- Говорим, что мы остаемся в состоянии атаки, так как враг еще в зоне поражения и ...
    return -- ... зачем-то выходим из функции, которая итак заканчивается
    end
    Аргументы: Отсутствуют
    Возвращает: Ничего
    Описание: Первым делом идут 2 проверки на возможность возвращения в состояние покоя. Одна проверяет, если цель больше вне зоны видимости, вторая- если цель уже убита. Третья проверка рассматривает тот случай, когда цель вышла из зоны дальности удара. В этом случае мы возвращается в состояние погони и приказываем гомункулу идти за целью. И наконец, если мы в зоне удара, то единственное, что нам осталось решить- использовать ли скилл, или бить простым ударом.




    Ну вот, собственно, весь AI пройден от начала до конца, однако, всеже, применения двух функций мы так и не увидили. Pазберем же их отдельно:


    GetTick()
    Она выдает число, которое отражает какое-то время в миллисекундах. Какое именно нам не важно знать, важно лишь, что если взять одно значение, а через некоторый отрезок времени еще одно, то их разница даст нам длинну этого отрезка времени в миллисекундах. Это можно использовать, например, когда вы хотите использовать скилл с каким-то промежутком времени:
    if (GetTick() > Время_последнего_применения + задержка) then
    *Применение скилла*
    Время_последнего_применения = GetTick()
    end
    Вы записываете время последнего применения скилла и затем сравниваете, если текущее время больше суммы задержки(в милли секундах) и времени последнего применения скилла, то скилл можно снова применять.

    GetV (HP|SP)
    В стандартном AI не было примеров применения функций для определения ХП и СП, но об их возможностях, я думаю, вы уже догадались. Стоит взглянуть на панель MirAI, и вы увидите, что многие вещи там зависят от текущего ХП/СП гомункула или хозяина. А проценты получить проще простого- делите текущее ХП/СП на максимальное и умножаете на 100.

    ---

    VIII. Полная хронология обработки скрипта

    Здесь я представлю полную картину обработки скрипта, от загрузки до исполнения, чтобы у вас было полноценное представление о том, что за чем идет:

    Если гомункул в свернутом/мертвом состоянии, либо вне зоны видимости, то скрипт не трогается вообще(клиент на него не обращает никакого внимания).
    Если гомункул появился из-за края экрана, либо его воскресили/вызвали, либо вы только появились в игре, использовали средства телепортации(и тп, что может являтся причиной первого появления гомункула), то клиент начинает загрузку скрипта.
    Первым делом он смотрит какой у вас стоит режим /hoai. Если стандартный, то он ищет файл AI. lua в папке AI, а если пользовательский, то файл AI.lua в папке AI/USER_AI.
    Клиент начинает загружать содержимое файла, строчка за строчкой, проверяя, чтобы код соответствовал правилам языка Lua.
    Если у вас вначале файла есть функции require, которые подключают другие файлы с кодами, то в первую очередь будут заргужены они, однако данной функцией можно заргужать код и в любом другом месте файла.
    Все объявления переменных, вызовы функций, действия и операции, которые будут присутствовать в файле вне рамок какой-либо функции, будут исполнены 1 раз до первого вызова функции AI, и больше никогда.
    Если в ходе загрузки файла будут найдены ошибки, то клиент будет постоянно пытаться заного загрузить файл. В данном случае надо оперативно исправить ошибку, сохранить изменения, и тогда окно ошибки перестанет вылетать, либо свернуть гомункула между вылетами окна ошибки.
    Далее в загруженном контексте ищется функция в глобальной переменной AI. Если таковой не находится, то будет выдана ошибка, и тогда все будет также, как в предыдущем пункте.
    Если же функция AI присутствует, то клиент начнет ее вызывать, примерно, от 1 до 7 раз в секунду, каждый раз по-очередно исполняя указанный в ней код.
    Важно также знать, что данные, которые скрипт получает с помощью функций GetV , GetActors и тп, обновляются только между вызовами клиентом функции AI.

    Пример:

    Загрузка скрипта и проверка его на синтаксические ошибки
    require "myfile. lua " -- Первым делом проверяется этот файл по той же схеме, что и данный

    -- Затем в переменные заносятся указанные значения
    local a = 1
    local b = "abc"

    c = {}

    function sum(a,b,c) -- Функция тоже значение для переменной sum- на данном этапе она загружается и проверяется на синтаксические ошибки, но не запускается

    return a + b + c
    end


    d = sum(3,2,1) -- Здесь уже загруженная функция будет вызвана один раз при загрузке скрипта и никогда больше

    function AI() -- Загружается и проверяется, но еще пока не запускается, главная функция

    neg(sum(1,2,3))
    end

    neg = function(a) -- Загружается и проверяется другая функция

    return -a
    end

    Работа скрипта
    require "myfile.lua"

    local a = 1
    local b = "abc"

    c = {}

    function sum(a,b,c)

    return a + b + c
    end


    d = sum(3,2,1)

    function AI()

    -- Когда все проверено и загружено, то начинает работу главная функция, и теперь, все действия, указанные в ней, будут выполнятся циклично, ...
    neg(sum(1,2,3)) -- ... тоесть будут вызываться данные функции
    end

    neg = function(a)

    return -a
    end


    ---

    IX. Помощь в начинаниях

    Итак, вы проштудировали весь гайд, ваша голова полна теории, но вы боитесь начать с нуля писать свой скрипт? Так давайте же вместе приступим к созданию вашего собственного скрипта!

    Для начала, нам нужно открыть SciTE и сделать чистый файл с названием AI.lua, сохранив его в папку AI(можете пока переместить стандартный AI в другое место). Теперь напишем самое главное- функцию AI, которая будет иметь следующий вид:

    function AI(id)
    end

    Наличие в файле AI.lua функции в глобальной переменной AI является единственным обязательным требованием для вашего скрипта. Агрумент id можно переименовать в любой другой, от названия ничего не зависет(можно даже не писать его, если вы сможете найти ID гомункула другими способами). Если сейчас сохранить изменения, запустить игру и переключиться на созданный вами скрипт, то он уже будет работать, однако гомункул будет просто стоять на месте, не воспринимая никаких команд. Теперь надо хорошо подумать, что же вы хотите, чтобы ваш гомункул делал, тоесть с какими ситуациями он может столкнуться и как он должен на них реагировать. Модель с состояниями, предложенная в стандартном AI, весьма неплоха и легка для понимания, поэтому я бы предложил взять ее как основу и заодно подключить к скрипту, уже знакомые нам, Const.lua и Util.lua. Таким образом, нам нужна будет глобальная переменная для хранения текущего состояния. Но, если вспомнить стандартный AI, то там имелась еще одна глобальная переменная- для хранения ID гомункула. Еще я бы предложил сделать отдельную переменную для ID алхимика, так как он тоже очень часто нужнен в разных местах скрипта. Суммируявышесказанное, пишемкод:

    require "./AI/Const. lua "
    require "./AI/Util.lua "

    State = 0 -- Объявляем переменную для текущего состояния
    HomunID = 0 -- Объявляем переменную для гомункула
    OwnerID = 0 -- Объявляем переменную для алхимика

    function AI(id)

    -- Следующей проверки нету в стандартной скрипте, а она там была бы очень кстати, ведь крайне глупо постоянно заносить одно и тоже число в одну и туже переменную, достаточно сделать это один раз:
    if (HomunID ~= id) then -- Если мы еще не занесли ID гомункула в глобальную переменную(тоесть скрипт только что запушен, и функция AI вызывается в первый раз), то ...
    HomunID = id -- ... заносим ее и ...
    OwnerID = GetV (V_OWNER,HomunID) -- ... тоже самое делаем с ID хозяина
    end
    end
    Arny, -=FaNaTiK=- и Владимир нравится это.
  5. maas

    maas Игрок

    Простейшая база заложена, но мы так и не добрались до каких-либо умных действий со стороны гомункула. Самое время дать ему хотя бы базовые умения. Начнем мы, пожалуй, с навыка следовать за хозяином, который реализуется добавлением пары строчек в конец функции AI:

    if (GetDistance2 (OwnerID,HomunID) > 2) then -- Если дальность до хозяина превышает 2 клетки, то ...
    MoveToOwner(HomunID) -- ... приказываем гомункулу идти к хозяину
    end

    Теперь ваш гомункул не просто тупо стоит на месте, а идет за вами, если вы отходите от него больше чем на 2 клетки. Но не забываем, что мы хотели сделать скрипт с состояниями. Сколько же их нужно? Посмотрим- наш гомункул уже умеет стоять на месте и идти за хозяином, значит нам понадобится как минимум 2 состояния- покой и следование. Назовем их также, как и в стандартном AI- IDLE_ST и FOLLOW_ST. При появлении наш гомункул будет в состоянии покоя, поэтому при объявлении переменной State мы сразу занесем туда состояние IDLE_ST. Затем нам нужна кострукция для перехода из одного состояния в другое- опять же, заимствуем ее из стандартного AI и пишем "лестницу" elseif. И последнее, что нам нужно- функции для обработки состояний. Итак, пишем:

    require "./AI/Const. lua "
    require "./AI/Util. lua "

    IDLE_ST = 0 -- Объявляем переменную для обозначения состояния покоя
    FOLLOW_ST = 1 -- Объявляем переменную для обозначения состояния следования

    State = IDLE_ST -- Объявляем переменную для текущего состояния
    HomunID = 0 -- Объявляем переменную для гомункула
    OwnerID = 0 -- Объявляем переменную для алхимика

    function AI(id)

    -- Следующей проверки нету в стандартной скрипте, а она там была бы очень кстати, ведь крайне глупо постоянно заносить одно и тоже число в одну и туже переменную, достаточно сделать это один раз:
    if (HomunID ~= id) then -- Если мы еще не занесли ID гомункула в глобальную переменную(тоесть скрипт только что запушен, и функция AI вызывается в первый раз), то ...
    HomunID = id -- ... заносим ее и ...
    OwnerID = GetV (V_OWNER,HomunID) -- ... тоже самое делаем с ID хозяина
    end

    if (GetDistance2 (OwnerID,HomunID) > 2) then -- Если дальность до хозяина превышает 2 клетки, то ...
    MoveToOwner(HomunID) -- ... приказываем гомункулу идти к хозяину
    end

    if (State == IDLE_ST) then -- Если текущее состояние- покой, то ...
    OnIDLE_ST() -- ... передаем усправление соответствующей функции
    elseif (State == FOLLOW_ST) then -- Если текущее состояние- следование, то ...
    OnFOLLOW_ST() -- ... передаем усправление соответствующей функции
    end
    end

    OnIDLE_ST() -- Функция для обработки состояния покоя

    end

    OnFOLLOW_ST() -- Функция для обработки состояния следования за хозяином

    end

    Конструкция имеется, но она пока ничего не делает. Чтобы гомункул использовал состояния нужно описать условия перехода из одного состояния в другое. В нашем случае это переход из покоя в следование и наоборот. Условия перехода покой-следование нам нужно записать в функцию IDLE_ST(), а следование-покой в FOLLOW_ST(), и, конечно же, после этого удалить из функции AI кусок кода, отвечающий за следования за хозяином:

    require "./AI/Const. lua "
    require "./AI/Util. lua "

    IDLE_ST = 0 -- Объявляем переменную для обозначения состояния покоя
    FOLLOW_ST = 1 -- Объявляем переменную для обозначения состояния следования

    State = IDLE_ST -- Объявляем переменную для текущего состояния
    HomunID = 0 -- Объявляем переменную для гомункула
    OwnerID = 0 -- Объявляем переменную для алхимика

    function AI(id)

    -- Следующей проверки нету в стандартной скрипте, а она там была бы очень кстати, ведь крайне глупо постоянно заносить одно и тоже число в одну и туже переменную, достаточно сделать это один раз:
    if (HomunID ~= id) then -- Если мы еще не занесли ID гомункула в глобальную переменную(тоесть скрипт только что запушен, и функция AI вызывается в первый раз), то ...
    HomunID = id -- ... заносим ее и ...
    OwnerID = GetV (V_OWNER,HomunID) -- ... тоже самое делаем с ID хозяина
    end

    if (State == IDLE_ST) then -- Если текущее состояние- покой, то ...
    OnIDLE_ST() -- ... передаем усправление соответствующей функции
    elseif (State == FOLLOW_ST) then -- Если текущее состояние- следование, то ...
    OnFOLLOW_ST() -- ... передаем усправление соответствующей функции
    end
    end

    function OnIDLE_ST() -- Функция для обработки состояния покоя

    if (GetDistance2 (OwnerID,HomunID) > 2) then -- Если дальность до хозяина превышает 2 клетки, то ...
    State = FOLLOW_ST -- ... меняем состояние на "следование"
    end
    end

    function OnFOLLOW_ST() -- Функция для обработки состояния следования за хозяином

    if (GetDistance2 (OwnerID,HomunID) < 3) then -- Если гомункул достаточно близко к хозяину, то ...
    State = IDLE_ST -- ... меняем состояние на "покой"
    else -- В противном случае ...
    MoveToOwner(HomunID) -- ... следуем за хозяином
    end
    end

    Как славно! Гомункул ходит за нами, используя модель состояний. Очень умным это назвать сложно, но посмотрите на количество кода- оно также очень мало. Чтобы начать извлекать практическую пользу из нашего питомца, нам нужно научить его драться. Причем глупо если он будет кидаться на всех подряд. А значит нам нужно, чтобы он кидался по команде! Но что, если мы захотим его снова отозвать? Ну, видимо, для этого тоже нужна команда. Итак, программа максимум для нас- сделать, чтобы гомункул понимал эти 2 команды. И как вы уже наверно догадались, такие команды имеются- ATTACK_CMD(двойной клик по цели) и FOLLOW_CMD(Alt+T). А факт их сообщения гомункулу мы можем зафиксировать через функцию GetMsg. Но вместе с командами растет и количество состояний- нам нужно состояние для атаки и для пассивности, которые мы можем позаимствовать из стандартного AI. Чуть не забыл- нужна также переменная для хранения текущей цели. Чтож, реализуемнашиидеивкоде:

    require "./AI/Const. lua "
    require "./AI/Util.lua "

    IDLE_ST = 0 -- Объявляем переменную для обозначения состояния покоя
    FOLLOW_ST = 1 -- Объявляем переменную для обозначения состояния следования
    ATTACK_ST = 2 -- Объявляем переменную для обозначения состояния атаки
    PASSIVE_ST = 3 -- Объявляем переменную для обозначения состояния пассивности

    State = IDLE_ST -- Объявляем переменную для текущего состояния
    Enemy = 0 -- Объявляем переменную для текущего врага
    HomunID = 0 -- Объявляем переменную для гомункула
    OwnerID = 0 -- Объявляем переменную для алхимика

    function AI(id)

    -- Следующей проверки нету в стандартной скрипте, а она там была бы очень кстати, ведь крайне глупо постоянно заносить одно и тоже число в одну и туже переменную, достаточно сделать это один раз:
    if (HomunID ~= id) then -- Если мы еще не занесли ID гомункула в глобальную переменную(тоесть скрипт только что запушен, и функция AI вызывается в первый раз), то ...
    HomunID = id -- ... заносим ее и ...
    OwnerID = GetV (V_OWNER,HomunID) -- ... тоже самое делаем с ID хозяина
    end

    local cmd = GetMsg (HomunID) -- Считываем текущую команду, посланную игроком гомункулу

    if (cmd[1] == ATTACK_OBJECT_CMD) then -- Если команда- атаковать цель, то ...
    State = ATTACK_ST -- ... меняем состояние на атаку и ...
    Enemy = cmd[2] -- ... считываем врага из второго параметра команды
    end

    if (cmd[1] == FOLLOW_CMD) then -- В противном случае если команда- пассивность, то ...
    State = PASSIVE_ST -- ... меняем состояние на пассивное
    end

    if (State == IDLE_ST) then -- Если текущее состояние- покой, то ...
    OnIDLE_ST() -- ... передаем усправление соответствующей функции
    elseif (State == FOLLOW_ST) then -- Если текущее состояние- следование, то ...
    OnFOLLOW_ST() -- ... передаем усправление соответствующей функции
    elseif (State == ATTACK_ST) then -- Если текущее состояние- атака, то ...
    OnATTACK_ST() -- ... передаем усправление соответствующей функции
    elseif (State == PASSIVE_ST) then -- Если текущее состояние- пассивность, то ...
    OnPASSIVE_ST() -- ... передаем усправление соответствующей функции
    end
    end

    function OnIDLE_ST() -- Функция для обработки состояния покоя

    if (GetDistance2 (OwnerID,HomunID) > 2) then -- Если дальность до хозяина превышает 2 клетки, то ...
    State = FOLLOW_ST -- ... меняем состояние на "следование"
    end
    end

    function OnFOLLOW_ST() -- Функция для обработки состояния следования за хозяином

    if (GetDistance2 (OwnerID,HomunID) < 3) then -- Если гомункул достаточно близко к хозяину, то ...
    State = IDLE_ST -- ... меняем состояние на "покой"
    else -- В противном случае ...
    MoveToOwner(HomunID) -- ... следуем за хозяином
    end
    end

    function OnATTACK_ST() -- Функция для обработки состояния следования атаки

    if ( GetV ( V _ MOTION ,Enemy) == MOTION_DEAD) then -- Если враг мертв или вне поля нашей видимости, то ...
    Enemy = 0 -- ... мы стираем его из памяти гомункула, ...
    State = IDLE_ST -- ... меняем состояние на "покой" и ...
    return -- ... выходим из функции
    end

    if (GetDistance2 (HomunID,Enemy) == -1) then -- Если враг вне поля нашей видимости, то ...
    State = IDLE_ST -- ... меняем состояние на "покой" и ...
    return -- ... выходим из функции
    end

    if (GetDistance2 (HomunID,Enemy) == 1) then -- Если расстояние до врага- 1 клетка (расстояние укуса гомункула), то ...
    Attack (HomunID,Enemy) -- ... атакуем его
    else -- В противном случае ...
    local x,y = GetV (V_POSITION,Enemy) -- ... считываем его координаты и ...
    Move (HomunID,x,y) -- ... следуем за ним
    end
    end

    function OnPASSIVE_ST() -- Функция для обработки состояния пассивности

    if (GetDistance2 (OwnerID,HomunID) > 2) then -- Если дальность до хозяина превышает 2 клетки, то ...
    MoveToOwner(HomunID) -- ... следуем за хозяином
    end
    end

    Последней изюминкой скрипта может быть добавление в состояния покоя и следования проверки на то, если убежавший от нас враг снова появился в зоне нашей видимости, чтобы гомункул смог пойти его добить без наших указаний. А делается это добавлением следующего куска кода в начала обоих этих состояний:

    if (GetDistance2 (HomunID,Enemy) ~= -1) then -- Если враг в зоне нашей видимости, то ...
    State = ATTACK_ST -- ... меняем состояние на атаку и ...
    return -- ... выходим из функции
    end

    Таким образом, у нас появился вполне рабочий скрипт, использующий модель состояний. Надеюсь, что данная глава помогла вам лучше понять логику создания скрипта, ибо показать все я не смогу, да и не хочу, так как данный гайд служит прежде всего для самообучения. Удачи Вам в ваших трудах!

    ---

    X. Примечание

    На момент написания гайда, версия Lua в клиенте Асгарда была 5.0, поэтому вся информация по языку дана в контексте этой версии. В случае обновления интерпретатора языка в клиенте, не исключены некоторые изменения в синтаксисе языка, что может привести к ошибкам в ваших скриптах, а также в MirAI, RampageAI и прочих, если их не обновят под новую версию. Какая версия на данный момент стоит в клиенте можно узнать, посмотрев стринг(например, вывести его в файл) в глобальной переменной _VERSION. За полной информацией по последней версии языка обращайтесь к мануалу.

    ---

    Вся информация гайда открыта для копирования и распространения. Желательно, но не обязательно, с указанием изначального автора и источника. Thx 4 Reedin' !

    точная ссылка на гайд - http://www.asgardro.com/forum/archive/index.php/t-11560.html
    Восемь, Arny и -=FaNaTiK=- нравится это.
  6. FrauMaus

    FrauMaus Игрок

    Этот гайд писал ~Sanctus~.
    И у меня даже есть его оригинальная копия с Асгарда: одной страницей, со структурой, с красивым форматированием, с разноцветным синтаксисом.
    Безымянный.png

    Вложения: