Как устроен AI на конечных автоматах: практический разбор FSM-схемы — Game Design Radar
← Все посты

Как устроен AI на конечных автоматах: практический разбор FSM-схемы

06.02.2026
Как устроен AI на конечных автоматах: практический разбор FSM-схемы

Автор разбирает практическую реализацию AI на конечных автоматах (Finite State Machines, FSM) для врагов, в частности — для логики управления вражескими самолетами: полёт, повороты, обход препятствий, реакция на игрока.

Структура событий FSM

Вводится единая структура события:

struct FSMEvent { uint32 ID; union { ExitArgs; }; };

У события есть ID и union с аргументами, специфичными для типа события (например, аргументы выхода из состояния и т.п.). Это позволяет типобезопасно передавать разные данные в рамках одного контейнера события.

Код обработки событий

Класс врага реализует метод HandleEvent:

uint32 SomeEnemy::HandleEvent( const FSMEvent& Event ) override { switch( Event.MakeSwitch(State) ) { default: break; } return Super::HandleEvent( Event ); };

Внутри используется switch по результату Event.MakeSwitch(State), который комбинирует текущее состояние и тип события в единый ключ. Для этого применяется макрос:

#define FSM_ID( SID, EID ) \ ( uint64(STATE_##SID) | (uint64(EVENT_STATE_##EID) << 32) )

Таким образом, каждая ветка switch может соответствовать конкретной паре «состояние + событие».

Смена состояний через возвращаемое значение

Ключевое решение: переходы между состояниями выполняются не через SetState(), а через возвращаемое значение HandleEvent. Ненулевое возвращаемое значение — это ID нового состояния.

Причины такого подхода:

  • Гарантируется, что смена состояния происходит после выхода из HandleEvent, а не внутри глубоко в стеке вызовов. Это разделяет «плаumbing» (смена состояния) и «логику» (обработка события).
  • Можно повторно вызывать обработчик событий и делать несколько переходов за один апдейт, пока логика того требует.
  • Можно гарантировать, что только одно состояние реально «потребляет» DeltaTime в течение одного апдейта, что важно для плавности движения и избежания стуттеров (нарушения линейности движения).

Внутренний метод смены состояния и апдейта выглядят так:

void SetState_Internal( uint32 InState ) { } void UpdateState( double DeltaTime ) { }

Точная реализация не приводится, но подразумевается, что внешний цикл апдейта вызывает HandleEvent, читает возвращаемый ID, вызывает SetState_Internal, при необходимости повторяет цикл, а затем вызывает UpdateState(DeltaTime) ровно один раз для финального состояния.

Если текущее состояние не обрабатывает событие, управление передаётся в базовый класс через Super::HandleEvent(Event). Это даёт возможность общих обработчиков по иерархии.

Автор подчёркивает, что это не универсальное решение, а рабочий компромисс, который показал себя лучше предыдущих, более «кривых» реализаций FSM.

Выводы

  • События FSM инкапсулируются в единую структуру с ID и типизированными аргументами.
  • Комбинация «состояние + событие» в единый ID упрощает switch по логике переходов.
  • Переходы реализуются через возвращаемое значение HandleEvent, а не прямой вызов SetState().
  • Такой подход позволяет делать несколько переходов за апдейт и гарантировать, что только одно состояние использует DeltaTime, снижая риск стуттеров.
  • Схема легко расширяется через базовый класс и не смешивает логику FSM с «плаumbing»-кодом в стеке вызовов.
check_circle Факт-чекинг
Статья прошла проверку. Фактологических ошибок не выявили.