Логика NPC, Логика NPC (Red75)

Материал из Mod Wiki.

(Различия между версиями)
Перейти к: навигация, поиск
Версия 12:10, 25 июля 2007 (править)
BAC9-FLCL (Обсуждение | вклад)

← К предыдущему изменению
Текущая версия (12:11, 25 июля 2007) (править) (отменить)
BAC9-FLCL (Обсуждение | вклад)
Логика NPC (Red75)» переименована в «Логика NPC»)
 
Строка 1: Строка 1:
-=Теория=+#REDIRECT [[Логика NPC]]
- +
-Предполагается, что читатель этой статьи знаком с языком LUA и основами объектно-ориентированного программирования.+
- +
-==История==+
- +
-Подход к решению проблемы игрового ИИ, выбранный создателями S.T.A.L.K.E.R. (далее буду писать просто "Сталкер"), был +
-впервые применён в 1957 году Гербертом Саймоном (Herbert Simon) и Алленом Ньюэллом (Allen Newell) в программе GPS +
-(General Problem Solver или Универсальный Решатель Задач).+
- +
-Суть этого подхода заключается в том, что сначала задача представляется в виде набора условий, набора операторов, +
-изменяющих эти условия, и описания начального и конечного состояний. Затем осуществляется поиск последовательности +
-операторов, переводящей начальное состояние системы в конечное.+
- +
-В Сталкере подсистема поиска последовательности операторов называется «планировщик».+
- +
-==ИИ в Сталкере==+
- +
-Как видно, этот способ решения проблем не подходит для шутера, так как ситуация в игре может меняться +
-непредсказуемым образом и построенный план решения (последовательность операторов) станет неприменимым к текущей +
-ситуации. Поэтому планировщик запускается каждый раз при непредвиденном развитии событий и создаёт новую +
-последовательность действий (операторов).+
- +
-Условия в игре тоже вычисляются динамически. Для этого используются специальные объекты – эвалуаторы. Эвалуатор +
-должен содержать метод evaluate(), возвращающий true, если условие выполняется и false в противном случае. Операторы +
-также представлены как объекты. Планировщик вызывает метод initialize() при начале работы оператора, затем он +
-периодически вызывает метод execute().+
- +
-Например, можно создать эвалуатор для условия «NPC голоден», и привязать к этому условию оператор «поесть». +
- +
-Планировщик будет периодически проверять это условие (вызывать метод evaluate() эвалуатора), и если оно выполняется, +
-инициализирует и будет выполнять оператор «поесть» до тех пор, пока условие не станет ложным.+
- +
-К сожалению, в большей части скриптов все возможности планировщика не используются. +
- +
-===Разбор настройки и работы планировщика на примере скрипта xr_kamp===+
- +
-Рассмотрим скрипт xr_kamp, заставляющий сталкеров сидеть у костра и рассказывать анекдоты.+
-Настройка планировщика осуществляется в функции add_to_binder. Параметры функции: object – объект для которого +
-настраивается планировщик (в нашем случае это сталкер), ini, scheme, section – инициализационный файл, название +
-схемы действий, секция ини-файла (эти параметры будут подробно разобраны в части по созданию мода), storage – +
-таблица для хранения текущих параметров схемы действий.+
- +
-Разберём, что делает эта функция. +
- +
-Сначала получаем планировщик для текущего объекта (object).+
- +
-<pre>local manager = object:motivation_action_manager()</pre>+
- +
-Затем присваиваем идентификаторы операторов и условий элементам массива. Это сделано просто для удобства. +
- +
-Идентификаторы могут иметь любое целочисленное значение, главное, чтобы они были уникальными, то есть не +
-использовались для других операторов и условий.+
- +
-<pre>properties["kamp_end"]=xr_evaluators_id.stohe_kamp_base+1+
-properties["on_position"]=xr_evaluators_id.stohe_kamp_base+2+
-properties["contact"]=xr_evaluators_id.stohe_meet_base+1+
-operators["go_position"]=xr_actions_id.stohe_kamp_base+1+
-operators["wait"]=xr_actions_id.stohe_kamp_base+3</pre>+
- +
-Для каждого идентификатора условия создадим соответствующий эвалуатор и добавим его в планировщик. В данном случае +
-это условия: «закончить ли посиделки около костра?» и «пришёл ли я на своё место у костра?»+
- +
-<pre>manager:add_evaluator (properties["kamp_end"], +
-this.evaluator_kamp_end ("kamp_end", storage, "kamp_end"))manager:add_evaluator (properties["on_position"], +
-this.evaluator_on_position ("kamp_on_position", storage, "kamp_on_position"))</pre>+
- +
-Теперь создадим оператор «сидеть около костра, рассказывать анекдоты, жевать колбасу и т.д.». Можно было бы +
-реализовать эти действия как набор разных операторов, выбором которых занимался бы планировщик, но автор скрипта +
-решил сделать один сложный оператор.+
- +
-<pre>local action = this.action_wait (object:name(),"action_kamp_wait", storage)</pre>+
- +
-Задаем предусловия для этого оператора. Планировщик выберет этот оператор при выполнении всех условий. Всё это +
-значит примерно следующее: я могу сидеть у костра, если: +
- +
-<pre>action:add_precondition (world_property(stalker_ids.property_alive, true))</pre>+
-я живой,+
- +
-<pre>action:add_precondition (world_property(stalker_ids.property_danger,false))</pre>+
-опасностей нет,+
- +
-<pre>action:add_precondition (world_property(stalker_ids.property_enemy, false))</pre>+
-врагов нет,+
- +
-<pre>action:add_precondition (world_property(stalker_ids.property_anomaly,false))</pre>+
-аномалий поблизости нет,+
- +
-<pre>xr_motivator.addCommonPrecondition(action)</pre>+
-выполняются другие важные условия (игрок не собирается со мной поговорить, я не собираюсь никого бить по морде, я не +
-ранен, я не собираюсь стрелять по вертолёту),+
- +
-<pre>action:add_precondition (world_property(properties["on_position"], true))</pre>+
-я уже нахожусь около костра.+
- +
-Скажем планировщику, что он должен ожидать от выполнения этого оператора. В данном случае после выполнения этого +
-оператора условие «закончить ли посиделки около костра?» должно стать истинным. То есть если условие стало истинным, +
-планировщик прекратит выполнение оператора.+
- +
-<pre>action:add_effect (world_property(properties["kamp_end"], true))</pre>+
- +
-Создание оператора завершено. Добавим его в планировщик.+
-<pre>manager:add_action (operators["wait"], action)</pre> +
- +
-Эта строчка не имеет отношения к работе планировщика. Если коротко, то она позволяет объекту получать уведомления об +
-определённых событиях (смерть NPC – вызывается метод death_callback(), попадание пули в NPC – вызывается метод +
-hit_callback() и т.д.)+
- +
-<pre>xr_logic.subscribe_action_for_events(object, storage, action)</pre>+
- +
-Создаем оператор, отвечающий за доставку NPC к его месту у костра.+
- +
-<pre>action = this.action_go_position (object:name(),"action_go_kamp", storage)</pre>+
- +
-Добавляем предусловия, как и для предыдущего оператора.+
- +
-<pre>action:add_precondition (world_property(stalker_ids.property_alive, true))+
-action:add_precondition (world_property(stalker_ids.property_danger,false))+
-action:add_precondition (world_property(stalker_ids.property_enemy, false))+
-action:add_precondition (world_property(stalker_ids.property_anomaly,false))+
-xr_motivator.addCommonPrecondition(action)+
-action:add_precondition (world_property(properties["on_position"], false))</pre>+
- +
-Единственное отличие – последнее условие. Этот оператор будет выполняться только если NPC ещё не находится на своем +
-месте у костра, то есть если функция evaluator_on_position.evaluate() возвращает false.+
- +
-В результате выполнения этого действия условие «на своём ли я месте у костра?» должно стать истинным.+
- +
-<pre>action:add_effect (world_property(properties["on_position"], true))</pre>+
- +
-Создание оператора завершено. Добавляем его к планировщику.+
- +
-<pre>manager:add_action (operators["go_position"], action)</pre>+
- +
-Осталось ещё одна задача. Нужно запретить планировщику активировать оператор «alife», тот самый оператор, который +
-заставляет NPC болтаться по карте, отстреливать собачек и в конце концов попадать в аномалию. Впрочем, отстрелом +
-врагов занимается другой оператор с идентификатором stalker_ids.action_combat_planner.+
- +
-Для этого мы получаем оператор «alife»+
- +
-<pre>action = manager:action (xr_actions_id.alife)</pre>+
- +
-И добавляем к его предусловиям следующее: условие «закончить ли посиделки у костра?» должно быть истинным.+
- +
-<pre>action:add_precondition (world_property(properties["kamp_end"], true))</pre>+
- +
-Итак, мы настроили планировщик. Посмотрим как всё это будет работать.+
- +
-В некоторый момент времени гулаг, в который попал NPC, назначает ему работу: сидеть у костра. В результате условие +
-«закончить ли посиделки у костра?» становится ложным. Планировщик видит это изменение и пытается выработать +
-последовательность операторов, после выполнения которой, условие бы стало истинным и NPC снова бы вернулся к +
-выполнению высокоприоритетного оператора «alife». Для выполнения этой задачи подходит оператор «посиделки у костра», +
-но для него не выполняется условие «я на своем месте у костра». Поэтому планировщик создаёт план из двух операторов: +
-«дойти до костра» и «посиделки у костра». Если во время выполнения одного из операторов возникнет непредвиденная +
-ситуация (появится враг, главный герой начнёт приставать с вопросами и т.п.), то планировщик скорректирует план, +
-добавив оператор для устранения этой непредвиденной ситуации.+
- +
-Как видно система ИИ в Сталкере обладает весьма большой гибкостью, что мы и продемонстрируем при создании мода.+
- +
-==Модели (или схемы) поведения в Сталкере==+
- +
-В наборе скриптов Сталкера предусмотрена возможность объединять операторы и условия в модели поведения. Модель +
-поведения – это набор логически связанных операторов и условий, служащих для выполнения определённой задачи. Так +
-скрипт xr_kamp представляет собой модель поведения, состоящую из двух операторов и двух условий.+
- +
-===Регистрация модели поведения===+
- +
-Для включения новой модели поведения в набор моделей, доступных NPC, сначала необходимо её зарегистрировать. +
-Предположим, нам нужно зарегистрировать модель поведения, описанную в скрипте actor_need_help.script. Регистрация +
-моделей осуществляется в скрипте modules.script. Добавим туда следующие строки:+
- +
-<pre>if actor_need_help then – в этой строке мы проверяем что наш скрипт действительно существует+
- load_scheme("actor_need_help", "actor_need_help", stype_stalker)+
-end</pre>+
- +
-Первый параметр функции load_scheme задает имя файла скрипта, второй параметр – это название модели поведения, +
-третий параметр – тип модели поведения (возможны следующие значения: stype_stalker – модель поведения NPC, +
-stype_mobile – модель поведения монстра, stype_item – «модель поведения» физического объекта, stype_heli – модель +
-поведения вертолёта, stype_restrictor – «модель поведения» области пространства). Скрипты для моделей поведения +
-разных типов пишутся по-разному. Мы будем рассматривать только модели поведения NPC.+
- +
-Внимание! Для успешной работы модели поведения её скрипт должен содержать функцию add_to_binder, выполняющую +
-настройку планировщика.+
- +
-===Активация/деактивация модели поведения===+
- +
-Некоторые модели поведения применимы в любых ситуациях (например, реакция на попадание пули в NPC или реакция на +
-появление врага). Такие модели должны активироваться/деактивироваться в функциях +
- +
-enable_generic_schemes()/disable_generic_schemes() скрипта xr_logic. В случае с моделью поведения actor_need_help, +
- +
-это будет выглядеть так:+
- +
-1. Создаём функции set_actor_need_help и disable_scheme в нашем скрипте actor_need_help. Эти функции будут отвечать +
-за активацию и деактивацию нашей модели поведения.+
- +
-<pre>function set_actor_need_help(npc,ini,scheme)+
- local st=xr_logic.assign_storage_and_bind(npc, ini, scheme, “actor_need_help”)+
- st.enabled=true+
-end+
-function disable_scheme(npc,scheme)+
- local st = db.storage[npc:id()][scheme]+
- if st then+
- st.enabled = false+
- end+
-end</pre>+
- +
-2. Добавляем следующую строку в скрипт xr_logic.script после строки «if stype == modules.stype_stalker then» в +
-функции enable_generic_schemes()+
- +
-<pre> actor_need_help.set_actor_need_help(npc,ini,”actor_need_help”)</pre>+
- +
-3. Добавляем следующую строку в скрипт xr_logic.script после строки «if stype == modules.stype_stalker then» в +
-функции disable_generic_schemes()+
- +
-<pre> actor_need_help.disable_scheme(npc,”actor_need_help”)</pre>+
- +
-Если же модель поведения предназначена только для использования в определённых ситуациях, то достаточно выполнить +
-шаг 1 и использовать созданные функции по мере надобности. Например, активируя эту схему через диалог с NPC (как мы +
-и сделаем в нашем моде).+
- +
-Внимание! Я максимально упростил функции активации/деактивации модели поведения. Чтобы полностью разобраться с ними, +
-посмотрите скрипты xr_combat, xr_kamp и другие подобные.+
- +
-===Приоритеты моделей поведения===+
- +
-Некоторые модели поведения настолько важны, что должны срабатывать в любой ситуации (например, реакция на попадание +
-пули). Для этого в скрипте xr_motivator предусмотрена функция addCommonPrecondition(action), в эту функцию можно +
-добавить одно из условий нашей модели поведения, чтобы другие модели поведения не могли сработать при выполнении +
-этого условия (здесь есть свои тонкости, но мы рассмотрим их позже). Предположим, что у нас есть модель поведения +
-actor_need_help, заставляющая NPC подбежать к ГГ и вылечить его. Пусть за проверку здоровья ГГ отвечает условие с +
-идентификатором actor_need_help.property_actor_is_wounded. Значит, если мы хотим, чтобы NPC подбегал к ГГ не обращая +
-внимание ни на что другое, то нужно добавить следующую строчку в функцию addCommonPrecondition(action):+
- +
-<pre>action:add_precondition(world_property(actor_need_help.property_actor_is_wounded,false))</pre>+
- +
-Эта строчка запретит выполнение всех других действий, если условие с идентификатором +
-actor_need_help.property_actor_is_wounded станет истинным (в нашем случае это будет означать, что ГГ сильно ранен. +
- +
-Конкретное значение здоровья ГГ при котором он считается сильно раненным будет определять эвалуатор этого условия).+
- +
- +
-=Создаем мод=+
- +
-В этом разделе мы сделаем мод, позволяющий сказать дружественно настроенному NPC, чтобы он лечил главного героя во +
-время боя.+
- +
-==Постановка задачи==+
- +
-Итак, мы хотим, чтобы дружественные NPC наконец начали приносить пользу. Для этого научим их лечить ГГ во время боя.+
-Распишем по пунктам:+
- +
-1. Нужно добавить дружественным NPC ветку в диалоге с просьбой присматривать за ГГ и лечить его, если в этом +
-возникнет необходимость.+
- +
-2. Добавить NPC модель поведения, реализующую выполнение этой просьбы.+
- +
-2.1. NPC должен действовать согласно этой модели только если ГГ находится недалеко от него.+
- +
-2.2. NPC не должен далеко отходить от ГГ во время боя.+
- +
-2.3. Если здоровье ГГ упало ниже определённой отметки, NPC должен подойти/подбежать и вылечить ГГ.+
- +
-==Что потребуется для реализации==+
- +
-Нам придётся изменять диалоги для некоторых NPC, для этого нужно будет изменить файлы +
-config\gameplay\character_dialogs.xml (диалоги для всех NPC), config\localization.ltx и config\system.ltx (подробнее +
-см. статью BAC9-FLCL или Fr3nzy). Мы изменим диалоги для всех NPC, но для неподходящих NPC диалог будет отсекаться с +
-помощью предусловия. Потребуется также добавить файлы с текстами диалогов и функции для проверки условий, +
-используемых в диалогах.+
- +
-Для включения новой модели поведения NPC нужно будет внести изменения в скрипты scripts\modules.script (регистрация +
-моделей поведения) и scripts\xr_motivator.script (для установки высокого приоритета нашей модели). Модификации файла +
-xr_logic.script, в котором происходит установка общих моделей поведения, не потребуется, так как мы будем +
-активировать нашу схему поведения при выборе определённой ветки в диалоге.+
- +
-Теперь решим какие условия и операторы нам понадобятся.+
- +
-Условия:+
- +
-1. Состояние главного героя. Если оно ниже определённого порога, то условие станет истинным. Назначим ему +
-идентификатор property_actor_is_wounded и эвалуатор evaluator_actor_is_wounded. Далее я буду указывать идентификатор +
-и эвалуатор в скобках через запятую.+
- +
-2. Находится ли NPC достаточно близко, чтобы вылечить ГГ. (property_ready_to_heal, evaluator_ready_to_heal)+
- +
-3. Есть ли у NPC аптечки. (property_has_medkit, evaluator_has_medkit)+
- +
-4. Не отошёл ли NPC слишком далеко от ГГ или ГГ от NPC. (property_faraway, evaluator_faraway)+
- +
-5. Находится ли ГГ достаточно близко, чтобы имело смысл помогать ему. (property_near_enough, evaluator_near_enough).+
- +
-Операторы:+
- +
-1. Лечить ГГ. (act_heal, action_heal)+
- +
-2. Подбежать к ГГ на дистанцию, достаточную для лечения (act_run_to_actor, action_run_to_actor)+
- +
-3. Крикнуть что аптечки кончились (act_no_medkit, action_no_medkit)+
- +
-4. Подобраться поближе к ГГ, чтобы быть под рукой. (act_stay_close, action_stay_close).+
- +
-==Реализация==+
- +
-Я буду писать эту статью параллельно с разработкой мода, указывая все найденные ошибки. Надеюсь, это поможет другим +
-моддерам. Чтобы отделить результаты тестирования от описания процесса разработки мода, я буду выделять свои +
-комментарии другим шрифтом.+
- +
-===Диалоги===+
- +
-В этом моде будет всего один диалог, и довольно простой, поэтому начнём с него. Создаём файл +
-config\gameplay\dialogs_need_help.xml. Чтобы не возиться с идентификаторами текстов попробуем обойтись без них. +
-Начнём с простой тестовой версии:+
- +
-<pre><?xml version="1.0" encoding="windows-1251" standalone="yes" ?>+
-<game_dialogs>+
- <dialog id="actor_will_need_help">+
- <precondition>actor_need_help.i_am_friend</precondition>+
- <phrase_list>+
- <phrase id="0">+
- <text>Дружище, сможешь присмотреть за мной, если стрелять начнут? Промедольчику, там, вколоть или +
- +
-перевязку сделать, если что...</text>+
- <next>1</next>+
- </phrase>+
- <phrase id="1">+
- <text>Нет проблем, конечно помогу.</text>+
- </phrase>+
- </phrase_list>+
- </dialog>+
-</game_dialogs></pre>+
- +
-Добавляем строку с идентификатором этого диалога в config\gameplay\character_dialogs.xml:+
- +
-<pre><actor_dialog>actor_will_need_help</actor_dialog></pre>+
- +
-Дописываем имя файла диалога в config\system.ltx в секцию «dialogs». Осталось создать функцию i_am_friend. Наш +
-скрипт с моделью поведения будет называться scripts\actor_need_help.script, заодно пропишем там и диалоговые +
-функции.+
- +
-<pre>function i_am_friend(actor,npc)+
- return npc:relation(actor)==game_object.friend+
-end</pre>+
- +
-При первом тестировании я заменил game_object.friend на game_object.neutral, чтобы не искать друзей по всей карте. +
- +
-Тестируем, что у нас получилось...+
- +
-Диалог работает, но вместо текста – набор значков, оказалось, я написал текст в кодировке CP866 (DOS), нужно +
-поменять её на CP1251. Так, теперь текст в порядке.+
- +
-Выбранный подход к созданию диалога оказался удачным.+
- +
-Обратите внимание, вместо идентификатора текста можно вписать сам текст. Это усложнит локализацию, но уменьшит время +
-создания диалогов.+
- +
-Теперь нам понадобятся функции проверки наличия аптечек у NPC, проверки активации схемы поведения, +
-активации/деактивации схемы поведения. Добавляем их в наш скрипт-файл.+
- +
-Первый вариант скрипта я не буду приводить, чтобы не увеличивать и так большую статью. Окончательный вариант +
-смотрите в конце статьи. Дальше по тексту идёт простейший работающий вариант.+
- +
-Теперь у нас должно быть два варианта начальной фразы для активной и неактивной схемы. Придётся экспериментировать.+
- +
-'Вот первый проверенный вариант:+
- +
-<pre><?xml version="1.0" encoding="windows-1251" standalone="yes" ?>+
-<game_dialogs>+
- <dialog id="actor_will_need_help">+
- <precondition>actor_need_help.i_am_friend</precondition>+
- <phrase_list>+
- <phrase id="0">+
- <next>1</next>+
- <next>2</next>+
- </phrase>+
- <phrase id="1">+
- <text>Дружище, сможешь присмотреть за мной, если стрелять начнут? Промедольчику, там, вколоть или +
- +
-перевязку сделать, если что...</text>+
- <precondition>actor_need_help.scheme_is_not_active</precondition>+
- <next>11</next>+
- <next>12</next>+
- </phrase>+
- <phrase id="11">+
- <text>Нет проблем, конечно помогу.</text>+
- <precondition>actor_need_help.npc_have_medkit</precondition>+
- <action>actor_need_help.activate_scheme</action>+
- </phrase>+
- <phrase id="12">+
- <text>Извини, друг, аптечек совсем не осталось.</text>+
- <precondition>actor_need_help.npc_havent_medkit</precondition>+
- </phrase>+
- <phrase id="2">+
- <text>Спасибо, друг, теперь я сам справлюсь.</text>+
- <precondition>actor_need_help.scheme_is_active</precondition>+
- <next>21</next>+
- </phrase>+
- <phrase id="21">+
- <text>Да не за что, ты мне помог, я - тебе.</text>+
- <action>actor_need_help.deactivate_scheme</action>+
- </phrase>+
- </phrase_list>+
- </dialog>+
-</game_dialogs></pre>+
- +
-При первом запуске игра вылетела без сообщений об ошибках. Я внимательно просмотрел все файлы и оказалось, что в +
-скриптах вместо комментария ‘--‘ (два минуса) я поставил просто минус (рекомендую пользоваться компилятором с +
-[www.lua.org www.lua.org] для проверки корректности скриптов). После исправления ошибки игра запустилась, но диалог так и не +
-появился. Небольшая дискуссия на форуме (спасибо Z.E.N. и Arhet) показала, что придётся сделать два диалога.+
- +
-Кроме того, при тестах первых вариантов, выяснилось, что всегда выбирается вариант диалога с просьбой о помощи. То +
-есть схема поведения не активируется. +
- +
-Как оказалось, в качестве параметров в функции передаются не объекты ГГ и NPC, а объекты говорящего в данный момент +
-персонажа и его собеседника. То есть, если фраза принадлежит NPC, то первый параметр будет объектом для NPC, а не +
-для ГГ. Поэтому я изменил названия параметров и переписал функции.+
- +
-Итак, все проблемы решены. Файл диалога принял следующий вид (config\gameplay\dialogs_need_help.xml):+
- +
-<pre><?xml version="1.0" encoding="windows-1251" standalone="yes" ?>+
-<game_dialogs>+
- <dialog id="actor_will_need_help">+
- <precondition>actor_need_help.i_am_friend</precondition>+
- <precondition>actor_need_help.scheme_is_not_active</precondition>+
- <phrase_list>+
- <phrase id="0">+
- <text>Дружище, сможешь присмотреть за мной, если стрелять начнут? Промедольчику, там, вколоть или +
- +
-перевязку сделать, если что...</text>+
- <next>11</next>+
- <next>12</next>+
- </phrase>+
- <phrase id="11">+
- <precondition>actor_need_help.npc_have_medkit</precondition>+
- <text>Нет проблем, конечно помогу.</text>+
- <action>actor_need_help.activate_scheme</action>+
- </phrase>+
- <phrase id="12">+
- <precondition>actor_need_help.npc_havent_medkit</precondition>+
- <text>Извини, друг, аптечек совсем не осталось.</text>+
- </phrase>+
- </phrase_list>+
- </dialog>+
- <dialog id="actor_will_not_need_help">+
- <precondition>actor_need_help.i_am_friend</precondition>+
- <precondition>actor_need_help.scheme_is_active</precondition>+
- <phrase_list>+
- <phrase id="0">+
- <text>Спасибо, друг, теперь я сам справлюсь.</text>+
- <next>21</next>+
- </phrase>+
- <phrase id="21">+
- <text>Да не за что, ты мне помог, я - тебе.</text>+
- <action>actor_need_help.deactivate_scheme</action>+
- </phrase>+
- </phrase_list>+
- </dialog>+
-</game_dialogs></pre>+
- +
-Функции, поддерживающие работу диалога, теперь выглядят так (файл scripts\actor_need_help.script): +
- +
-<pre>function i_am_friend(talker,target)+
- return target:relation(talker)==game_object.friend+
-end+
- +
--- За основу этой функции взята функция dialogs.actor_have_medkit+
-function npc_have_medkit(talker, target)+
- return talker:object("medkit") ~= nil or+
- talker:object("medkit_army") ~= nil or+
- talker:object("medkit_scientic") ~= nil+
-end+
-function npc_havent_medkit(talker, target)+
- return not npc_have_medkit(talker,target)+
-end+
- +
--- Так как модель поведения еще не написана, вставим заглушки+
-local scheme_status={}+
-function scheme_is_active(talker,target)+
- return scheme_status[target:id()]==true -- сравниваем с true, чтобы функция не возвращала nil+
-end+
-function scheme_is_not_active(talker,target)+
- return not scheme_is_active(talker,target)+
-end+
-function activate_scheme(talker,target)+
- scheme_status[talker:id()]=true+
-end+
-function deactivate_scheme(talker,target)+
- scheme_status[talker:id()]=nil -- присваиваем nil, чтобы освободить память, занятую этим элементом массива+
-end</pre>+
- +
-И в файл config\gameplay\character_dialogs.xml добавлены строки:+
- +
-<pre><actor_dialog>actor_will_need_help</actor_dialog>+
-<actor_dialog>actor_will_not_need_help</actor_dialog></pre>+
- +
-В результате получился работающий диалог, но NPC выглядит просто как ходячая аптечка. В окончательном варианте, я +
-добавил некоторые «человеческие» реакции. Да и сама модель поведения пока отсутствует - вместо неё стоят заглушки. Исправим это.+
- +
-===Модель поведения===+
- +
-Начнём создание модели поведения с разработки эвалуаторов. Эвалуатор должен представлять собой объект класса унаследованного от класса property_evaluator. +
- +
-Возьмём для начала эвалуатор evaluator_faraway определяющий, что NPC находится слишком далеко от ГГ. Этот эвалуатор требуется для того, чтобы NPC не отходил слишком далеко от ГГ и мог в случае надобности быстро подбежать к нему и оказать помощь. +
- +
-Объявляем класс эвалуатора:+
- +
-<pre>class "evaluator_faraway" (property_evaluator)</pre>+
- +
-Определяем функцию инициализации (в LUA это аналог конструктора объекта)+
- +
-<pre>function evaluator_faraway:__init(name, storage) super (nil, name)+
- self.st = storage+
-end</pre>+
- +
-Ключевое слово «super» служит для вызова конструктора базового класса. Член «st» будет хранить ссылку на таблицу состояния нашей модели поведения.+
- +
-Теперь нужно определить функцию evaluate(), ради которой и создавался эвалуатор. По-видимому всё просто, нужно проверить расстояние от NPC до ГГ и вернуть true, если это расстояние больше определённого значения. Но давайте подумаем. Когда эвалуатор возвратит true, заработает оператор, заставляющий NPC подойти поближе к ГГ, то есть расстояние моментально уменьшится и эвалуатор начнёт возвращать false, что приведёт к переходу NPC под управление игрового ИИ. ИИ может опять решить удалиться от ГГ, что приведёт к повторному срабатыванию эвалуатора. В результате возникнет замкнутый цикл, и NPC будет крутиться на одном месте (на самом деле этот цикл рано или поздно разорвётся из-за изменения игровой ситуации, но лучше вообще избежать его).+
- +
-Можно использовать разные пути для решения этой проблемы. Попробуем сделать так: будем использовать два расстояния, эвалуатор сработает при достижении первого и будет оставаться активным, пока расстояние не станет меньше второго.+
- +
-<pre>local min_faraway_dist=10+
-local max_faraway_dist=20+
- +
-function evaluator_faraway:evaluate()+
- local actor=db.actor+
- if not actor then+
- -- ГГ ещё не заспаунился+
- return false+
- end+
- local dist=actor:position():distance_to(self.object:position())+
- if dist>max_faraway_dist then+
- self.st.faraway=true+
- elseif dist<min_faraway_dist then+
- self.st.faraway=false+
- end+
- return self.st.faraway==true+
-end</pre>+
- +
-Эвалуатор готов, но нужно как-то его протестировать. Поэтому давайте создадим минимальную модель поведения из одного условия и одного оператора. Нам нужен оператор, перемещающий NPC поближе к ГГ. Объявляем класс action_stay_close, унаследованный от action_base, и определяем его конструктор.+
- +
-<pre>class "action_stay_close" (action_base)+
- +
-function action_stay_close:__init(name, storage) super (nil, name)+
- self.st=storage+
-end</pre>+
- +
-Оператор должен содержать функции initialize(), execute() и, возможно, finalize().+
- +
-Функция initialize() вызывается при начала работы оператора, то есть в момент, когда планировщик ставит этот оператор в первую позицию плана.+
- +
-<pre>function action_stay_close:initialize()+
- local npc=self.object+
- -- Не знаю зачем эти две функции, но они используются во всех операторах+
- npc:set_desired_position()+
- npc:set_desired_direction()+
- -- Сбрасываем текущие анимации+
- npc:clear_animations()+
- -- Задаём параметры движения+
- npc:set_detail_path_type(move.line)+
- npc:set_body_state(move.standing)+
- npc:set_movement_type(move.run)+
- npc:set_path_type(game_object.level_path)+
- -- Эксперименты показали, что эта функция устанавливает скорость движения (anim.danger - минимальная скорость, anim.free - +
-нормальная, anim.panic - максимальная)+
- npc:set_mental_state(anim.panic)+
- -- Повышаем зоркость NPC+
- npc:set_sight(look.danger, nil, 0)+
- -- Освободим сталкера от всех идиотских ограничений+
- npc:remove_all_restrictions()+
--- Зададим смещение точки назначения, чтобы помощники не сбивались в кучу+
- self.offset=vector():set(math.random()*6-3,0,math.random()*6-3)+
- self.offset:normalize()+
-end</pre>+
- +
-Функция execute() периодически вызывается во время выполнения оператора. Частота вызовов, по-видимому, зависит от расстояния от NPC до ГГ.+
- +
-<pre>function action_stay_close:execute()+
- local npc=self.object+
- local actor=db.actor+
- if not actor then+
- -- Хм, что-то не так. Может быть ГГ перешёл на другой уровень? Запрещаем схему поведения+
- self.st.enabled=false+
- end+
- -- Получаем ближайшую доступную точку в 5 метрах от ГГ+
- -- Сначала, я попробавал использовать функцию npc:vertex_in_direction, но она не работает+
- local vertex_id=level.vertex_in_direction(actor:level_vertex_id(),self.offset,5)+
- local act_v_id=actor:level_vertex_id()+
- -- Отправляем нашего NPC в найденную точку+
- local acc_id=utils.send_to_nearest_accessible_vertex( npc, vertex_id )+
- if self.st.dist and self.st.dist>max_faraway_dist then+
--- если NPC находится слишком далеко от ГГ пусть пробежиться побыстрее+
- npc:set_mental_state(anim.panic)+
- else+
- npc:set_mental_state(anim.free)+
- end+
-end</pre>+
- +
-Функция настройки планировщика.+
- +
-<pre>function add_to_binder(object, char_ini, scheme, section, st)+
- local manager = object:motivation_action_manager()+
- local property_wounded = xr_evaluators_id.sidor_wounded_base+
- +
- -- Удаляем эвалуатор, так как в xr_motivator мы установили его в property_evaluator_const+
- manager:remove_evaluator(property_faraway)+
- -- и заменяем его нашим+
- manager:add_evaluator(property_faraway, evaluator_faraway("evaluator_faraway",st))+
- -- Создаём оператор+
- local action = action_stay_close("action_stay_close",st)+
- -- и настраиваем предусловия. 1. Сталкер жив+
- action:add_precondition(world_property(stalker_ids.property_alive, true))+
- -- 2. Сталкер не ранен+
- action:add_precondition(world_property(property_wounded, false))+
- -- Я использую свой мод для обхода аномалий, иначе от помощников мало толку.+
- if anomaly_evader then+
- -- 3. Рядом нет аномалий+
- action:add_precondition (world_property(1099,false))+
- end+
- -- 4. Сталкер слишком далеко от ГГ+
- action:add_precondition(world_property(property_faraway, true))+
- action:add_effect (world_property(property_faraway, false))+
- -- Добавляем оператор в планировшик+
- manager:add_action (act_stay_close, action)+
- -- Теперь подкорректируем стандартные операторы, чтобы помощник не отвлекался на всякую ерунду.+
- action=manager:action(stalker_ids.action_alife_planner)+
- action:add_precondition(world_property(property_faraway, false))+
- action=manager:action(stalker_ids.action_combat_planner)+
- action:add_precondition(world_property(property_faraway, false))+
- action=manager:action(stalker_ids.action_danger_planner)+
- action:add_precondition(world_property(property_faraway, false))+
-end</pre>+
- +
-Добавим функции активации/деактивации схемы поведения.+
- +
-<pre>function set_help(npc, ini)+
- local st = xr_logic.assign_storage_and_bind(npc, ini, "actor_need_help")+
- st.enabled=true+
-end+
-function disable_scheme(npc, scheme)+
- local st = db.storage[npc:id()][scheme]+
- if st then+
- st.enabled = false+
- end+
-end</pre>+
- +
-Изменим диалоговые функции-заглушки.+
- +
-<pre>function activate_scheme(talker,target)+
- set_help(talker,talker:spawn_ini())+
- scheme_status[talker:id()]=true+
-end+
-function deactivate_scheme(talker,target)+
- disable_scheme(talker,"actor_need_help")+
- scheme_status[talker:id()]=nil+
-end</pre>+
- +
-Добавим в функцию xr_motivator.addCommonPrecondition() следующие строки, чтобы заблокировать стандартные схемы поведения.+
- +
-<pre> if actor_need_help then+
- action:add_precondition (world_property(actor_need_help.property_faraway,false))+
- end</pre>+
- +
-Если попробовать запустить мод сейчас, то игра просто вылетит. Причина в том, что мы добавили предусловие для стандартных схем поведения, но не добавили эвалуатор этого условия. Поэтому добавляем в функцию xr_motivator.net_spawn() следующие строки:+
- +
-<pre> local manager = self.object:motivation_action_manager()+
- if actor_need_help then+
- manager:add_evaluator(actor_need_help.property_faraway, property_evaluator_const(false))+
- end</pre>+
- +
-Для того чтобы снизить нагрузку на процессор, используем property_evaluator_const, который всегда возвращает одно и тоже значение.+
-В результате тестирования выяснилось, что не все NPC подчиняются нашей схеме поведения. Причины этого пока не ясны, требуется дополнительное тестирование и очень желательна помощь разработчиков (хотя бы для того чтобы узнать как выяснить какой оператор действует в данный момент на NPC).+
- +
-===Файлы мода===+
- +
-Мод можно скачать [[:Media:Напарники 0.2.zip|здесь]].+
- +
-Ссылка обновлена. Выложена новая версия. Причиной "зависаний" NPC был конфликт со state_mgr.script. Код мода, приведённый в статье, не устанавливал состояние NPC с помощью state_mgr.set_state(), в результате state_mgr пытался вернуть состояние NPC к начальному (до срабатывания нашей модели поведения), что и приводило к "зависаниям".+
- +
-Более подробное описание мода смотри [[Mod:Напарники|здесь]].+
- +
-----+
- +
-Спасибо за внимание,+
-Red75+
- +
-==Авторы==+
- +
-Статья создана:+
- +
-* [[Участник:Red75|Red75]]+
- +
-[[Категория:Статьи участников]]+

Текущая версия

  1. REDIRECT Логика NPC
Личные инструменты