Враг с простым ИИ: конечный автомат
Простой ИИ врага удобно описать как конечный автомат: набор состояний и правил перехода между ними.
Суть: враг в каждый момент находится в одном состоянии (патруль, погоня, атака), а условия переключают его между ними.
Враг, который всегда делает одно и то же, скучный. Враг, в котором запутанный код с кучей флагов, — кошмар для отладки. Золотая середина — конечный автомат (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, держи переходы в одном месте, делай их в обе стороны и начинай с двух-трёх состояний.