Сповіщення
Очистити все

SoC. Логіка NPC. Частина 1


Ранг:
Майстер
Роль:
Гість
Записи:
752
Приєднався:
2 роки тому
 

Теорія

Передбачається, що читач цієї статті знайомий із мовою LUA та основами об'єктно-орієнтованого програмування.

 

Історія

Підхід до вирішення проблеми ігрового ІІ, обраний творцями STALKER (далі писатиму просто "Сталкер"), був вперше застосований в 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 ):

 
local manager = object:motivation_action_manager ( )
 

Потім надають ідентифікатори операторів та умов елементам масиву. Це просто для зручності.

Ідентифікатори можуть мати будь-яке ціле значення, головне, щоб вони були унікальними, тобто не використовувалися для інших операторів та умов.

 
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 operation _base +1 operators [ "wait" ] =xr_actions_id.stohe_kamp_base +3
 

Для кожного ідентифікатора умови створимо відповідний евалуатор та додамо його до планувальника. У цьому випадку це умови: чи закінчити посиденьки біля багаття? і чи прийшов я на своє місце біля багаття? .

 
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" ) _
 

Тепер створимо оператора сидіти біля багаття, розповідати анекдоти, жувати ковбасу тощо. . Можна було б реалізувати ці дії як набір різних операторів, вибором яких займався планувальник, але автор скрипту вирішив зробити один складний оператор.

 
local action = this.action_wait ( object:name ( ) , "action_kamp_wait" , storage )
 

Задаємо передумови для цього оператора. Планувальник вибере цей оператор під час всіх умов. Все це означає приблизно таке: я можу сидіти біля багаття, якщо:

 
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" ] ,   true ) )
 

я вже перебуваю біля багаття.

Скажімо, планувальнику, що він повинен чекати від виконання цього оператора. У цьому випадку після виконання цього оператора умова чи закінчити посиденьки біля багаття? має стати справжнім. Тобто, якщо умова стала істинною, планувальник припинить виконання оператора.

 
action:add_effect      ( world_property ( properties [ "kamp_end" ] ,    true ) )
 

Створення оператора завершено. Додамо його до планувальника.

 
manager:add_action ( operators [ "wait" ] , action )
 

Цей рядок не має відношення до роботи планувальника. Якщо коротко, вона дозволяє об'єкту отримувати повідомлення про певні події (смерть NPC – викликається метод death_callback() , потрапляння кулі в NPC – викликається метод hit_callback() тощо.)

 
xr_logic.subscribe_action_for_events ( object, storage, action )
 

Створюємо оператор, який відповідає за доставку NPC до його місця біля багаття.

 
action = this.action_go_position ( object:name ( ) , "action_go_kamp" , storage )
 

Додаємо передумови, як і попереднього оператора.

 
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 ( world_property ( stalker_en ) tion ( world_property ( stalker_ids.property_anomaly, false ) ) 
xr_motivator.addCommonPrecondition ( action ) 
action : add_precondition    ( world_property ( properties [ " on_position " ] ,   false ) )
 

Єдина відмінність – остання умова. Цей оператор буде виконуватися лише якщо NPC ще не знаходиться на своєму місці біля вогнища, тобто якщо функція evaluator_on_position.evaluate() повертає false .

В результаті виконання цієї дії умова на своєму місці я біля вогнища? має стати справжнім.

 
action:add_effect      ( world_property ( properties [ "on_position" ] ,   true ) )
 

Створення оператора завершено. Додаємо його до планувальника.

 
manager:add_action ( operators [ "go_position" ] , action )
 

Залишилося ще одне завдання. Потрібно заборонити планувальнику активувати оператор alife , той самий оператор, який змушує NPC бовтатися по карті, відстрілювати собачок і врешті-решт потрапляти в аномалію. Втім, відстрілом ворогів займається інший оператор із ідентифікатором stalker_ids.action_combat_planner .

Для цього ми отримуємо оператор alife :

 
action = manager:action ( xr_actions_id.alife )
 

І додаємо до його передумов наступне: чи умова закінчити посиденьки біля вогнища? має бути істинним.

 
action:add_precondition    ( world_property ( properties [ "kamp_end" ] ,    true ) )
 

Отже, ми налаштували планувальника. Подивимося, як усе це працюватиме.

У певний момент часу гулаг, у який потрапив NPC, призначає йому роботу: сидіти біля вогнища. В результаті умова чи закінчити посиденьки біля багаття? стає хибним. Планувальник бачить цю зміну і намагається виробити послідовність операторів, після виконання якої умова стала б істинною і NPC знову б повернувся до виконання високопріоритетного оператора alife . Для виконання цього завдання підходить оператор посиденьки біля багаття , але для нього не виконується умова на своєму місці біля багаття . Тому планувальник створює план із двох операторів: дійти до багаття та посиденьки біля багаття . Якщо під час виконання одного з операторів виникне непередбачена ситуація (з'явиться ворог, головний герой почне чіплятися з питаннями тощо), то планувальник скоригує план, додавши оператора для усунення цієї непередбаченої ситуації.

Як видно, система ІІ в Сталкері має дуже велику гнучкість, що ми і продемонструємо при створенні мода.


   
Цитата