Автор разбирает практическую реализацию 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»-кодом в стеке вызовов.