Стаття
Розробка ігор · Патерни проєктування ШІ · ⏱ ≈ 12 хв читання · Останнє оновлення: 23 червня 2026 р.

Дерева поведінки для ШІ NPC — проєктування, тик і дошка

Дерева поведінки (BT) — домінантна архітектура складного ігрового ШІ, яку використовують у Halo, Unreal Engine та стеках ROS. Порівняно зі скінченними автоматами, BT компонуються природно: піддерева перевикористовуються, а декоратори модульні. Розуміння протоколу тику, основних типів вузлів і дошки достатньо, щоб будувати складну поведінку NPC з нуля.

1. Чому FSM не справляються

Скінченний автомат (FSM) моделює ШІ як граф: вузли — це стани, ребра — переходи. Це працює для простих персонажів, але швидко погіршується:

Складність станів FSM: T = O(S²) переходів для S станів Складність піддерева BT: T = O(S log S) за ієрархічного повторного використання Ключова думка: BT виражають пріоритет явно через структуру дерева → крайній лівий нащадок = найвищий пріоритет у селекторі → батьківська послідовність блокує всіх нащадків, доки один не зазнає невдачі

2. Чотири основні типи вузлів

Послідовність / Sequence (→)

Виконує нащадків зліва направо. Одразу повертає FAILURE, якщо будь-який нащадок зазнає невдачі. Повертає SUCCESS лише якщо всі успішні. Як логічне AND.

Селектор / Selector (?)

Виконує нащадків зліва направо. Повертає SUCCESS на першому успіху. Повертає FAILURE лише якщо всі зазнали невдачі. Як логічне OR з пріоритетом.

Лист / Дія (Action)

Реалізує власне поведінку: MoveTo, Attack, PlayAnim. Повертає SUCCESS, FAILURE або RUNNING (для багатокадрових задач).

Умова (Condition)

Лист, що запитує стан світу. IsEnemyVisible? HasAmmo? Повертає SUCCESS або FAILURE синхронно, ніколи RUNNING.

Послідовність: тикати до першого FAILURE або всіх SUCCESS [Patrol → SeeEnemy? → Chase → Attack] Якщо patrol успішний, але SeeEnemy ні → Sequence = FAILURE → батьківський селектор пробує наступний варіант Селектор: тикати до першого SUCCESS [AttackIfCanSee? → InvestigateNoise? → Patrol] Пріоритет: атака > розслідування > патрулювання Якщо умова атаки не справджується, пробує розслідувати

3. Протокол тику та потік статусів

Кожен ігровий кадр: root.tick(blackboard) → поширюється в глибину, повертає: SUCCESS | FAILURE | RUNNING Статус RUNNING: дія багатокадрова (пошук шляху, анімація) Наступний тик повертається в дерево безпосередньо до запущеного вузла Варіанти пам'яті: BT без стану: перетикається з кореня щокадру → простіше, може переривати дії одразу Зі станом (з пам'яттю): запам'ятовує, який нащадок виконувався → відновлюється з того нащадка; нащадки не переоцінюються, доки не завершаться Повторний вхід без стану щокадру зазвичай кращий, бо дозволяє умовам із вищим пріоритетом переривати поточні дії (помічено ворога → перервати патруль на півдорозі) Частота тику: 10–60 Hz для ігор у реальному часі, 1–5 Hz для роботів ROS

4. Дошка — спільна пам'ять

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

Патерн дошки: Сприйняття: SightSensor записує bb.set("enemy_pos", {x,y}) Умова: IsEnemyVisible → читає bb.get("enemy_visible") Дія: MoveTo → читає bb.get("move_target") Область: глобальна (увесь NPC), піддерево (тимчасовий локальний контекст) Область піддерева дозволяє тому самому вузлу SubTree виконуватися паралельно на різних цілях завдяки окремим областям дошки Керування пам'яттю: очищати застарілі записи, коли дії завершуються, або використовувати TTL (час життя) для записів сенсорів Спостерігачі дошки: дії можуть слухати зміни й повертати RUNNING, доки не надійде подія → уникає опитування щокадру для низькочастотних подій

5. Декоратори та повторне використання піддерев

Декоратори: вузли-обгортки з одним нащадком, що модифікують поведінку Inverter: повертає протилежність до статусу нащадка (вентиль NOT) Repeater: перезапускає нащадка N разів або доки він не зазнає невдачі (цикли) Succeeder: завжди повертає SUCCESS незалежно від нащадка ForceFailure: завжди повертає FAILURE незалежно від нащадка Декоратор Cooldown: якщо останнє виконання < період перезарядки → одразу повернути FAILURE Інакше: тикати нащадка й записати часову мітку Until Fail / Until Success: повторювати нащадка до зазначеного результату SubTree: посилання на зовнішньо визначене дерево → імпортувати піддерево RetreatBehavior у кількох персонажів → змінити визначення піддерева один раз — оновляться всі персонажі Приклад: дерево бою з обмеженням частоти Selector ├── Cooldown(0.5s) → Sequence │ ├── IsEnemyInMeleeRange │ └── MeleeAttack └── Sequence ├── IsEnemyVisible └── ShootAtEnemy

6. BT проти FSM — компроміси проєктування

Переваги BT

Ієрархічні, модульні, візуальне налагодження, природний пріоритет, без «спагеті» переходів, легке повторне використання піддерев, галузевий стандарт у AAA-ігровому ШІ.

Переваги FSM

Простіші за дуже малої кількості станів, детерміновані переходи, тривіальні в реалізації, без накладних витрат на тики, легко формально верифікувати.

Обмеження BT

Перетикання може бути затратним, якщо дерево величезне. BT без стану можуть «миготіти» між діями, якщо умови змінюються щокадру.

Альтернатива GOAP

Цілеспрямоване планування дій (Goal-Oriented Action Planning) планує послідовності під час виконання за допомогою A*. Гнучкіше за BT для складних ієрархій цілей (використано у F.E.A.R.).

7. Рушій дерева поведінки на JavaScript

// Мінімальний синхронний рушій дерева поведінки
const Status = Object.freeze({ SUCCESS: 'S', FAILURE: 'F', RUNNING: 'R' });

class Sequence {
  constructor(...children) { this.children = children; }
  tick(bb) {
    for (const c of this.children) {
      const s = c.tick(bb);
      if (s !== Status.SUCCESS) return s;
    }
    return Status.SUCCESS;
  }
}

class Selector {
  constructor(...children) { this.children = children; }
  tick(bb) {
    for (const c of this.children) {
      const s = c.tick(bb);
      if (s !== Status.FAILURE) return s;
    }
    return Status.FAILURE;
  }
}

class Action {
  constructor(fn) { this.fn = fn; }
  tick(bb) { return this.fn(bb); }
}

class Condition {
  constructor(pred) { this.pred = pred; }
  tick(bb) { return this.pred(bb) ? Status.SUCCESS : Status.FAILURE; }
}

class Inverter {
  constructor(child) { this.child = child; }
  tick(bb) {
    const s = this.child.tick(bb);
    if (s === Status.SUCCESS) return Status.FAILURE;
    if (s === Status.FAILURE) return Status.SUCCESS;
    return Status.RUNNING;
  }
}

class Blackboard {
  constructor() { this.data = {}; }
  set(k, v) { this.data[k] = v; }
  get(k) { return this.data[k]; }
  has(k) { return k in this.data; }
}

// Приклад: дерево NPC-вартового
const tree = new Selector(
  new Sequence(
    new Condition(bb => bb.get('enemy_visible')),
    new Action(bb => { console.log('Chasing enemy'); return Status.RUNNING; })
  ),
  new Sequence(
    new Condition(bb => bb.get('noise_heard')),
    new Action(bb => { console.log('Investigating'); return Status.SUCCESS; })
  ),
  new Action(bb => { console.log('Patrolling'); return Status.RUNNING; })
);

const bb = new Blackboard();
bb.set('enemy_visible', false);
bb.set('noise_heard', true);
tree.tick(bb);  // → "Investigating" (Розслідування)

8. Паралельні вузли, подієво-орієнтовані BT та гібриди HFSM