Враг с простым ИИ: конечный автомат

Простой ИИ врага удобно описать как конечный автомат: набор состояний и правил перехода между ними.

Суть: враг в каждый момент находится в одном состоянии (патруль, погоня, атака), а условия переключают его между ними.

Враг, который всегда делает одно и то же, скучный. Враг, в котором запутанный код с кучей флагов, — кошмар для отладки. Золотая середина — конечный автомат (FSM, finite state machine). Идея проста: у врага есть несколько состояний, в каждый момент он ровно в одном, и есть чёткие правила, когда переходить из одного в другое. Например: патрулирует, пока не увидел игрока; увидел — переходит в погоню; подобрался близко — атакует; игрок убежал — снова патруль.

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

  [ПАТРУЛЬ] --увидел игрока--> [ПОГОНЯ]
      ^                            |
      |                     игрок близко
   игрок ушёл                      v
      |                        [АТАКА]
      +----------------------------+

В коде это обычно match по текущему состоянию: для каждого состояния — своё поведение и свои проверки переходов.

extends CharacterBody2D

enum State { PATROL, CHASE, ATTACK }
var state: State = State.PATROL

func _physics_process(delta: float) -> void:
    match state:
        State.PATROL:
            if can_see_player():
                state = State.CHASE
        State.CHASE:
            if player_close():
                state = State.ATTACK
        State.ATTACK:
            if not player_close():
                state = State.CHASE

Полезно осознать, что конечный автомат — это не игровой трюк, а фундаментальная идея из информатики, которая встречается повсюду: от светофора до меню телефона. Светофор — это автомат с состояниями «красный», «жёлтый», «зелёный» и правилами перехода по таймеру. Стоит научиться видеть автоматы вокруг, и описывать поведение врагов, дверей, ловушек и даже экранов игры станет легко. Любая система, которая бывает в нескольких чётких режимах и переключается между ними по понятным условиям, прекрасно ложится на автомат — а значит, становится предсказуемой и простой в отладке.

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

Конечный автомат хранит одну переменную — текущее состояние. Каждый кадр он выполняет поведение этого состояния и проверяет условия перехода. Переход — это просто присваивание новой строки/значения переменной состояния. Никакой магии: вся «умность» врага сводится к таблице «в состоянии X при условии Y перейти в Z».

def update(state, distance):
    if state == "PATROL":
        if distance < 200:
            return "CHASE"
    elif state == "CHASE":
        if distance < 40:
            return "ATTACK"
        if distance > 300:
            return "PATROL"
    elif state == "ATTACK":
        if distance > 40:
            return "CHASE"
    return state

state = "PATROL"
for dist in [250, 150, 30, 30, 120, 400]:
    state = update(state, dist)
    print(f"Дистанция {dist:3} -> состояние {state}")

Та же логика на Python ▶. Враг переключается между состояниями только по дистанции до игрока. Проследи цепочку: подошёл — погоня, вплотную — атака, убежал — снова патруль. Это и есть весь «искусственный интеллект».

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

Первая ошибка — обойтись без автомата и налепить десяток флагов (is_chasing, is_attacking, was_patrolling): они начинают противоречить друг другу, и враг ведёт себя странно. Автомат гарантирует ровно одно состояние. Вторая — забыть обратные переходы: враг уходит в погоню, но никогда не возвращается в патруль, потому что нет условия «игрок убежал». Третья — менять состояние в нескольких местах кода: держи переходы в одном месте, иначе не отследишь логику. Четвёртая — слишком много состояний на старте: начни с двух-трёх.

Best practices

Опиши состояния через enum — так их имена читаемы и защищены от опечаток. Держи всю логику переходов в одном месте (обычно match в _physics_process). Делай переходы в обе стороны, чтобы враг не застревал. Начинай с минимума состояний и добавляй по мере надобности. Условия перехода выноси в маленькие функции с понятными именами (can_see_player, player_close) — код читается как описание поведения.

Итоги: простой ИИ врага — это конечный автомат: одно текущее состояние плюс правила переходов. В коде это match по состоянию. Под капотом — переменная состояния и таблица переходов. Используй enum, держи переходы в одном месте, делай их в обе стороны и начинай с двух-трёх состояний.

Проверьте себя
1. Что такое конечный автомат (FSM) для ИИ врага?
AСлучайные действия каждый кадр
BНабор состояний, в одном из которых враг находится, и правила перехода между ними
CОгромный список if без структуры
DФизическое тело врага
2. Почему набор флагов вроде is_chasing, is_attacking хуже автомата?
AФлаги занимают больше памяти
BФлаги могут противоречить друг другу, а автомат гарантирует ровно одно состояние
CФлаги нельзя использовать в GDScript
DАвтомат работает быстрее всегда