Поведение врага: конечный автомат (FSM)

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

Суть: поведение врага удобно описать как конечный автомат (FSM, Finite State Machine): набор состояний (Покой, Патруль, Погоня, Атака) и переходов между ними по условиям. В каждый момент враг находится ровно в одном состоянии и действует согласно ему.

Если писать ИИ врага кучей вложенных if, код быстро превращается в кашу. Лучше мыслить состояниями. Враг может быть в одном из: Покой (стоит), Патруль (ходит туда-сюда), Погоня (бежит за героем), Атака (бьёт). Это и есть конечный автомат: в любой момент он в одном состоянии, и есть правила перехода между ними.

Как работает под капотом

Конечный автомат врага (FSM):

  [Покой] --герой близко--> [Погоня] --дистанция атаки--> [Атака]
     ^                          |                            |
     |                          | герой убежал               | герой далеко
     |                          v                            v
  таймер вышел -> [Патруль] <---+----------------------------+

В каждом состоянии своё поведение; переход — по УСЛОВИЮ.

Главное достоинство FSM — ясность. Каждое состояние делает одну вещь, переходы явные. Добавить новое поведение — значит добавить состояние и переходы к нему, не ломая остальные.

public enum EnemyState { Idle, Patrol, Chase, Attack }

public class EnemyAI : MonoBehaviour
{
    [SerializeField] private float chaseRange = 5f;
    [SerializeField] private float attackRange = 1.5f;
    private EnemyState state = EnemyState.Patrol;

    void Update()
    {
        float dist = DistanceToPlayer();
        switch (state)
        {
            case EnemyState.Patrol:
                if (dist < chaseRange) state = EnemyState.Chase;
                break;
            case EnemyState.Chase:
                if (dist < attackRange) state = EnemyState.Attack;
                else if (dist > chaseRange) state = EnemyState.Patrol;
                break;
            case EnemyState.Attack:
                if (dist > attackRange) state = EnemyState.Chase;
                break;
        }
    }

    float DistanceToPlayer() { return 3f; }   // упрощённо
}

FSM языко-независим. Реализуем тот же автомат врага на чистом Python и прогоним по разным дистанциям до героя:

# Состояния врага и переходы по дистанции до героя
def next_state(state, dist, chase=5.0, attack=1.5):
    if state == "Patrol":
        if dist < chase:
            return "Chase"
    elif state == "Chase":
        if dist < attack:
            return "Attack"
        elif dist > chase:
            return "Patrol"
    elif state == "Attack":
        if dist > attack:
            return "Chase"
    return state

state = "Patrol"
# Герой постепенно подходит, потом убегает
for dist in [8, 4, 1, 1, 3, 9]:
    state = next_state(state, dist)
    print(f"дистанция {dist} -> состояние {state}")

Та же логика на Python ▶ — видно, как враг переходит Patrol → Chase → Attack по мере приближения героя и обратно, когда тот убегает. Это и есть «мозг» врага: одно состояние в момент времени плюс правила перехода.

Частые ошибки

  • Гигантский if-else вместо состояний. Без FSM логика ИИ становится нечитаемой уже на трёх вариантах поведения.
  • Забыть обратные переходы. Враг входит в Погоню, но не выходит из неё — гонится вечно. Каждому переходу нужен обратный.
  • Считать дистанцию каждый кадр дорого. Сравнивай квадрат расстояния с квадратом радиуса — это избегает медленного корня.
  • Дёргать переход на самой границе. Если порог один и тот же, враг «мерцает» между состояниями. Делай зону погони чуть шире зоны выхода (гистерезис).

Best practices

  • Описывай ИИ через явные состояния и переходы — это читаемо и расширяемо.
  • Используй enum для состояний, чтобы не путаться в строках.
  • Сравнивай квадраты расстояний (sqrMagnitude) вместо вычисления корня — это быстрее.

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

Куда растёт FSM дальше

Конечный автомат отлично работает, пока состояний немного. Но когда враг умеет десятки вещей, переходы между всеми состояниями превращаются в запутанную паутину «каждый со всеми». Индустрия придумала на это ответы, и полезно знать, что они есть. Иерархические автоматы группируют состояния: например, общее состояние «Бой» внутри себя содержит «Погоня» и «Атака», и переходы упрощаются. Behavior Trees (деревья поведения) описывают ИИ как дерево задач с приоритетами — это стандарт в больших играх. Но не спеши к ним: для платформера или аркады простого FSM из четырёх состояний хватает с запасом, и преждевременное усложнение только вредит. Сначала доведи игру до играбельности на простом автомате, а более сложные модели подключай, только когда поведение врага реально упрётся в потолок FSM.

Проверьте себя
1. Что такое конечный автомат (FSM) в поведении врага?
AСлучайный выбор действий
BНабор состояний и переходов между ними по условиям; враг всегда в одном состоянии
CСписок всех врагов
DТип коллайдера
2. Почему сравнивают квадрат расстояния, а не само расстояние?
AТак точнее
BЧтобы избежать медленного вычисления квадратного корня
CЭто требование Unity
DЧтобы враг был сильнее