Попередження дерев поведінки


25

Я намагаюся обвести голову навколо дерев поведінки, тому я вискакую деякий тестовий код. Одне з чим я борюся - це як викупити поточний вузол, що працює, коли відбувається щось більш високий пріоритет.

Розглянемо наступне просте, вигадане дерево поведінки для солдата:

введіть тут опис зображення

Припустимо, що якась кількість кліщів пройшла повз, а поруч не було ворога, солдат стояв на траві, тому вузол Сидіти вибрано для виконання:

введіть тут опис зображення

Тепер для дії « Сідайте» потрібен час для виконання, оскільки для цього є анімація, тому вона повертається Runningяк її статус. Тик чи два проходить, анімація все ще працює, але Ворог поруч? спрацьовує вузол стану. Тепер нам потрібно стягнути якнайшвидший вузол Сидіти, щоб ми могли виконати вузол Attack . В ідеалі солдат навіть не закінчить сідати - він може замість цього змінити напрямок анімації, якби він тільки почав сидіти. Для додаткового реалізму, якщо він минув якийсь переломний момент в анімації, ми можемо замість цього вибрати, щоб дозволити йому закінчити сісти, а потім знову встати, або, можливо, змусити його спіткнутися в поспіху реагувати на загрозу.

Постарайтеся, я не зміг знайти вказівки щодо вирішення подібної ситуації. Вся література та відеозаписи, які я споживав за останні кілька днів (а їх було багато), схоже, вирішують цю проблему. Найближче, що мені вдалося знайти, - це концепція скидання запущених вузлів, але це не дає вузлам на зразок Сидіти шансу сказати "ей, я ще не закінчив!"

Я думав, можливо, визначити a Preempt()чи Interrupt()метод на своєму базовому Nodeкласі. Різні вузли можуть впоратися з цим, як вони вважають за потрібне, але в цьому випадку ми спробуємо якнайшвидше повернути солдата на ноги, а потім повернутися Success. Я думаю, що цей підхід також вимагає, щоб моя база Nodeмала концепцію умов окремо від інших дій. Таким чином, двигун може перевіряти лише умови і, якщо вони проходять, викупити будь-який виконуваний в даний момент вузол перед початком виконання дій. Якщо ця диференціація не була встановлена, двигун повинен був би виконати вузли без розбору і, отже, міг запустити нову дію перед тим, як випереджати запущений.

Для довідки, нижче мої поточні базові класи. Знову ж таки, це шип, тому я намагався зберегти речі максимально просто і додаю складності лише тоді, коли мені це потрібно, і коли я це розумію, з чим я зараз борюся.

public enum ExecuteResult
{
    // node needs more time to run on next tick
    Running,

    // node completed successfully
    Succeeded,

    // node failed to complete
    Failed
}

public abstract class Node<TAgent>
{
    public abstract ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard);
}

public abstract class DecoratorNode<TAgent> : Node<TAgent>
{
    private readonly Node<TAgent> child;

    protected DecoratorNode(Node<TAgent> child)
    {
        this.child = child;
    }

    protected Node<TAgent> Child
    {
        get { return this.child; }
    }
}

public abstract class CompositeNode<TAgent> : Node<TAgent>
{
    private readonly Node<TAgent>[] children;

    protected CompositeNode(IEnumerable<Node<TAgent>> children)
    {
        this.children = children.ToArray();
    }

    protected Node<TAgent>[] Children
    {
        get { return this.children; }
    }
}

public abstract class ConditionNode<TAgent> : Node<TAgent>
{
    private readonly bool invert;

    protected ConditionNode()
        : this(false)
    {
    }

    protected ConditionNode(bool invert)
    {
        this.invert = invert;
    }

    public sealed override ExecuteResult Execute(TimeSpan elapsed, TAgent agent, Blackboard blackboard)
    {
        var result = this.CheckCondition(agent, blackboard);

        if (this.invert)
        {
            result = !result;
        }

        return result ? ExecuteResult.Succeeded : ExecuteResult.Failed;
    }

    protected abstract bool CheckCondition(TAgent agent, Blackboard blackboard);
}

public abstract class ActionNode<TAgent> : Node<TAgent>
{
}

Хтось має розуміння, яке могло б направити мене в правильному напрямку? Чи моє мислення по правильній лінії, чи це наївно, як я боюся?


Потрібно ознайомитись з цим документом: chrishecker.com/My_liner_notes_for_spore/… тут він пояснює, як по дереву ходить, не як державна машина, а з ROOT у кожного галочки, що є справжньою хитрістю до реактивності. BT не повинен потребувати винятків чи подій. Вони об'єднують системи по суті, і реагують на всі ситуації завдяки тому, що завжди стікають з кореня. Ось як працює превенція, якщо зовнішня умова більш високого пріоритету перевіряється, вона надходить туди. (виклик деякого Stop()зворотного дзвінка перед виходом із активних вузлів)
v.oddou

Цей aigamedev.com/open/article/popular-behavior-tree-design також дуже докладно деталізований
v.oddou

Відповіді:


6

Я виявив, що я задаю те саме питання, що і ви, і я провів чудову коротку розмову в розділі коментарів цієї сторінки блогу, де мені було надано ще одне рішення проблеми.

Перше - використовувати паралельний вузол. Паралельний вузол - це особливий тип складеного вузла. Він складається з послідовності перевірок передумов, за якими слідує один вузол дії. Він оновлює всі дочірні вузли, навіть якщо вузол дії знаходиться у стані "запуску". (На відміну від вузла послідовності, який повинен починати оновлення з поточного запущеного дочірнього вузла.)

Основна ідея полягає у створенні ще двох повернених станів для вузлів дії: "скасування" та "скасовано".

Збій перевірки передумов у паралельному вузлі - це механізм, який запускає скасування його запущеного вузла дії. Якщо вузол дії не вимагає тривалої логіки скасування, він негайно поверне "скасований". В іншому випадку він переходить у стан "скасування", де ви можете ввести всю необхідну логіку для правильного переривання дії.


Привіт і ласкаво просимо на GDSE. Було б чудово, якби ви могли відкрити цю відповідь із цього блогу тут і, врешті-решт, посилання на цей блог. Посилання мають тенденцію до вмирання, повний відповідь тут робить його більш стійким. Зараз питання має 8 голосів, тому хороша відповідь була б приголомшливою.
Кату

Я не думаю, що все, що повертає дерева поведінки до кінцевої машини, є гарним рішенням. Ваш підхід мені здається таким, що вам потрібно передбачити всі умови виходу кожної держави. Коли це насправді недолік FSM! BT має перевагу, починаючи з кореня, це створює повністю пов'язаний FSM неявно, уникаючи нас чітко записувати умови виходу.
v.oddou

5

Я думаю, що ваш солдат може бути розкладений у душі та тілі (і що б там не було). Згодом тіло може розкластись на ноги та руки. Потім для кожної частини потрібне власне дерево поведінки, а також загальнодоступний інтерфейс - для запитів з вищих або нижчих рівнів.

Отже, замість мікроуправління кожною окремою дією ви просто надсилаєте миттєві повідомлення типу "тіло, сідайте на деякий час" або "тіло, біжіть туди", і тіло буде керувати анімаціями, переходами стану, затримками та іншими речами для ти.

Крім того, організм може самостійно керувати такою поведінкою. Якщо у нього немає наказів, він може запитати «чи можемо ми тут сидіти?». Що ще цікавіше, через інкапсуляцію ви можете легко моделювати такі функції, як втома або оглушення.

Ви можете навіть обмінюватися частинами - зробити слона з інтелектом зомбі, додати крила людині (він навіть не помічає) або що-небудь ще.

Без такого розкладання, я думаю, що ви ризикуєте рано чи пізно зустріти комбінаторний вибух.

Також: http://www.valvesoftware.com/publications/2009/ai_systems_of_l4d_mike_booth.pdf


Спасибі. Прочитавши вашу відповідь 3 рази, я думаю, що я розумію. Я прочитаю цей PDF у ці вихідні.
мені--

1
Подумавши про це протягом минулої години, я не впевнений, що розумію різницю між наявністю повністю окремих БТ для розуму і тіла проти одного БТ, який розкладається на під деревами (на які посилається спеціальний декоратор, зі сценаріями побудови) зв’язуючи все разом в один великий БТ). Мені здається, що це може забезпечити подібні переваги від абстрагування і, власне, полегшить розуміння того, як поводиться дана сутність, оскільки вам не доведеться переглядати кілька окремих БТ. Однак я, мабуть, наївний.
я--

@ user13414 Різниця полягає в тому, що для створення дерева вам знадобляться спеціальні сценарії, коли просто використання непрямого доступу (тобто, коли вузол тіла повинен запитати його дерево, який об'єкт представляє ніжки) може бути достатньо, а також не потребуватиме додаткового епізоду. Менше коду, менше помилок. Крім того, ви втратите здатність (легко) перемикати піддерево під час виконання. Навіть якщо вам не потрібна така гнучкість, ви нічого не втратите (включаючи швидкість виконання).
Shadows In Rain

3

Лежачи в ліжку вчора ввечері, у мене було щось божевільне, як я можу займатися цим, не вводячи складності, до якої я схилявся у своєму питанні. Він передбачає використання (паралельно) композиту (погано названий ІМХО). Ось що я думаю:

введіть тут опис зображення

Сподіваємось, це все ще досить читабельно. Важливі моменти:

  • Сядьте / Delay / Встань послідовність є послідовністю в паралельній послідовності ( A ). На кожному галочку паралельна послідовність також перевіряє стан Ворога поблизу (перевернуто). Якщо ворог знаходиться поруч, умова провалюється, і так само робиться вся паралельна послідовність (негайно, навіть якщо дочірня послідовність знаходиться посередині через Сісти , Затримати або Встати )
  • при відмові селектор В над паралельною послідовністю стрибне вниз в селектор С для обробки переривання. Важливо, що селектор C не запускався, якби паралельна послідовність A завершена успішно
  • Потім селектор C намагається встати нормально, але може також викликати анімацію спотикання, якщо солдат знаходиться в занадто незручному положенні, щоб просто встати

Я думаю, що це спрацює (я скоро спробую це в моєму шипі), незважаючи на те, що я дещо брудніший, ніж я передбачав. Хороша річ у тому, що я врешті-решт зможу інкапсулювати під деревами як багаторазові логіки та звертатися до них з різних точок. Це полегшить більшу частину моєї турботи, тому я думаю, що це прийнятне рішення.

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

ОНОВЛЕННЯ : хоча цей підхід технічно працює, я вирішив, що це сукс. Це тому, що неспоріднені під деревами повинні "знати" про умови, визначені в інших частинах дерева, щоб вони могли викликати їхню власну смерть. Хоча обмін посиланнями на суб-дерево може певним чином полегшити цю біль, це все ще суперечить тому, що можна очікувати, дивлячись на дерево поведінки. Дійсно, я зробив одну і ту ж помилку двічі на дуже простому шипі.

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


1
Якщо ви дійсно хочете повторно використовувати subtrees, то логіка, коли потрібно перерватись ("ворог поблизу" тут), імовірно, не повинна бути частиною піддерева. Замість цього, можливо, система може попросити будь-яке піддерево (наприклад, B тут) перервати себе через стимул більш високого пріоритету, і тоді воно перейде на спеціально позначений вузол переривання (C тут), який обробляє повернення персонажа до деякого стандартного стану , наприклад, стоячи. Трохи схожий на дерево поведінки, еквівалент керування винятками.
Натан Рід

1
Ви навіть можете включити декілька обробників перерв залежно від того, який стимул перериває. Наприклад, якщо NPC сидить і починає вести вогонь, ви, можливо, не хочете, щоб він вставав (і ставив перед собою більшу ціль), а скоріше залишався низьким і намагався прикритися.
Натан Рід

@Nathan: смішно ви згадуєте про "обробку винятків". Першим можливим підходом, який я придумав минулої ночі, була така ідея композиційного вибору, у якого було б двоє дітей: одна для нормального виконання та одна для попереднього виконання. Якщо нормальна дитина проходить або провалюється, результат поширюється. Дитина, що проживає за попереднім дозволом, коли-небудь запускатиметься лише в тому випадку, якщо відбудеться виплата. У всіх вузлах був би Preempt()метод, який пронизав би дерево. Однак єдиним, що дійсно "впорається" з цим, буде композиційний пристрій, який миттєво переходитиме на свій дочірній вузол викупу.
я--

Тоді я подумав про паралельний підхід, який я окреслював вище, і це видалося більш елегантним, оскільки він не потребує додаткової чіткості в усьому API. На ваш погляд на інкапсуляцію під дерев, я думаю, де б не виникала складність, це було б можливим пунктом заміни. Це навіть може бути, коли у вас є кілька умов, які часто перевіряються разом. У цьому випадку корінь заміщення був би складовою послідовності з декількома умовами як її дочірні.
я--

Я думаю, що субтрети, знаючи умови, які їм потрібно "вдарити" перед виконанням, цілком доречно, оскільки це робить їх самодостатніми та дуже явними проти неявних. Якщо це викликає велике занепокоєння, тоді не тримайте умови всередині піддерева, а на "сайті виклику" цього.
Сейван

2

Ось рішення, яке я вирішив на даний момент ...

  • У моєму базовому Nodeкласі є Interruptметод, який за замовчуванням нічого не робить
  • Умови - це "першокласні" конструкції, оскільки вони зобов'язані повернутися bool(таким чином, маючи на увазі, що вони швидко виконуються і ніколи не потребують більше одного оновлення)
  • Node виставляє колекцію умов окремо своєму зібранню дочірніх вузлів
  • Node.Executeвиконує всі умови спочатку і виходить з ладу відразу, якщо будь-яка умова не вдається. Якщо умови досягли успіху (або таких немає), він викликає, ExecuteCoreщоб підклас міг виконати свою фактичну роботу. Є параметр, який дозволяє пропускати умови з причин, які ви побачите нижче
  • Node також дозволяє умови виконувати ізольовано через a CheckConditions методом. Звичайно, Node.Executeнасправді просто дзвінки, CheckConditionsколи потрібно перевірити умови
  • Моя Selector композиція тепер вимагає CheckConditionsкожної дитини, яку вона вважає за виконання. Якщо умови не вдається, він рухається прямо до наступної дитини. Якщо вони пройдуть, він перевіряє, чи вже є виконавець. Якщо так, то дзвонитьInterrupt а потім виходить з ладу. Це все, що він може зробити в цей момент, сподіваючись, що поточний запущений вузол відповість на запит переривання, який він може зробити за ...
  • Я додав Interruptible вузол, який є своєрідним декоратором, тому що він має регулярний потік логіки, як його прикрашена дитина, а потім окремий вузол для перебоїв. Він виконує свою звичайну дитину до завершення чи відмови, доки вона не буде перервана. Якщо він перерваний, він негайно переходить на виконання свого дочірнього вузла обробки переривань, який може бути настільки ж складним під деревом, як потрібно

Кінцевий результат - це щось подібне, узяте з мого шипа:

введіть тут опис зображення

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

введіть тут опис зображення

Якби цей вузол не був переривним, він ніколи не вийде з ладу, тому бджола б постійно блукала. Однак, оскільки батьківський вузол є селектором і має більш високі пріоритетні діти, їх придатність для виконання постійно перевіряється. Якщо їх умови пройдуть, селектор піднімає перерву, і під-дерево вище вище негайно переходить на шлях "Перерваний", який просто піддається ASAP, не виходячи з ладу. Звичайно, спочатку можна виконати інші дії, але мій шип насправді не має нічого іншого, крім застави.

Однак, щоб прив'язати це до мого запитання, ви можете собі уявити, що "Перерваний" шлях може спробувати змінити анімацію, що сидить, і, якщо цього не зробити, змусити солдата спіткнутися. Все це затримало б перехід до стану вищого пріоритету, і саме це і було метою.

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


1

Я виправив те саме питання, винайшов декоратор "Коли". У ньому є стан та дві поведінки дитини ("тоді" та "інакше"). Коли виконується "Коли", він перевіряє стан і залежно від його результату запускається тоді / інакше дочірня. Якщо зміна результату стану, запущена дитина скидається і запускається дитина, що відповідає іншій гілці. Якщо дитина закінчує виконання, ціле "Коли" завершує виконання.

Ключовим моментом є те, що на відміну від початкового BT у цьому питанні, коли стан перевіряється лише на початку послідовності, мій "Коли" продовжує перевіряти стан, поки він працює. Отже, верхівка дерева поведінки замінюється на:

When[EnemyNear]
  Then
    AttackSequence
  Otherwise
    When[StandingOnGrass]
      Then
        IdleSequence
      Otherwise
        Hum a tune

Для більш досконалого використання "Коли", ви також хочете ввести "Зачекайте" дії, які просто не роблять нічого протягом визначеного часу або на невизначений час (до скидання поведінкою батьків). Крім того, якщо вам потрібна лише одна гілка "Коли", інша може містити або "Успіх", або "Невдача", це відповідно вдасться і невдало провалиться.


Я думаю, що цей підхід ближчий до того, що мали на увазі оригінальні винахідники БТ. Він використовує більш динамічний потік, саме тому "запущений" стан у БТ - це дуже небезпечний стан, який слід використовувати дуже рідко. Ми повинні проектувати БТ завжди, маючи на увазі можливість повернутися в корені в будь-який час.
v.oddou

0

Поки я спізнююсь, але сподіваюся, що це може допомогти. Переважно тому, що я хочу переконатися, що особисто я щось не пропустив, тому що я намагався також зрозуміти це. Я здебільшого цю ідею запозичив у Unreal, але не роблячи її Decoratorвласністю на базіNode або сильно пов'язаного зBlackboard , вона є більш загальною.

Це введе новий тип вузла, який називається Guardкомбінацією a Decorator, Compositeі має acondition() -> Result підпис поряд із anupdate() -> Result

Він має три режими, які вказують, як має відбуватися скасування при GuardповерненніSuccess або Failed, власне скасування залежить від виклику. Отже, для Selectorвиклику Guard:

  1. Скасувати .self -> Скасуйте Guard(та його запущену дитину) лише тоді, коли він працює і умова булаFailed
  2. Скасувати .lower-> Скасуйте вузли нижчого пріоритету лише у тому випадку, якщо вони запущені, а стан був SuccessабоRunning
  3. Скасувати .both -> Обидва .selfта .lowerзалежно від умов та запущених вузлів. Ви хочете скасувати власну дію, якщо її запускається, і вмовився б falseабо скасувати запущений вузол, якщо вони вважаються нижчим пріоритетом на основі Compositeправила (Selector у нашому випадку), якщо умова єSuccess . Іншими словами, це в основному обидва поняття разом.

Подібно до Decorator і на відміну від цьогоComposite нього бере лише одна дитина.

Хоча Guardприймати тільки одну дитину, Ви можете вкладати так багато Sequences, Selectorsабо інших типів , Nodesяк ви хочете, в тому числі інший Guardsабо Decorators.

Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Sequence2 StandingOnGrass? Idle HumATune

У вищезазначеному сценарії щоразу, коли Selector1оновлення, він завжди буде перевіряти стан охоронців, пов’язаних із його дітьми. У наведеному вище випадкуSequence1 захищено, і його потрібно перевірити, перш ніж Selector1продовжувати виконувати runningзавдання.

Щоразу Selector2або Sequence1працює, як тільки EnemyNear?повернеться successпід час Guards condition()перевірки, тоді Selector1буде видано переривання / скасування наrunning node а потім продовжити як завжди.

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

Це також дозволяє захистити синглів, Nodeякі мають більший пріоритет, від запуску Nodesв одному і тому жComposite

Selector1 Guard.both[Sequence[EnemyNear?]] Sequence1 MoveToEnemy Attack Selector2 Guard.both[StandingOnGrass?] Idle HumATune

Якщо HumATuneтривалий біг Node, Selector2завжди спочатку перевіряйте це, якщо не було Guard. Тож якщо npc телепортується на трав'яний патч, наступного разу Selector2запускається, він перевірятиме Guardта скасовуєHumATune , щоб запуститиIdle

Якщо він телепортується з трав'яного патча, він скасує запущений вузол ( Idle) і перейде доHumATune

Як ви бачите тут, прийняття рішень покладається на абонента, Guardа не самого Guardсебе. Правила того, хто вважається таким, lower priorityзалишається у абонента. В обох прикладах саме той Selectorвизначає, що являє собою аlower priority .

Якби у вас був Compositeдзвінок Random Selector, ви отримаєте можливість визначити правила в рамках реалізації цього конкретного Composite.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.