Простой ИИ на C#: машина состояний
- О чём эта тема
- Первый противник с поведением: камера слежения замечает игрока, охранник преследует его по навигационному мешу и успокаивается по таймеру — простой ИИ как машина состояний.
- Аннотация
- Конспект строит стелс-миниатюру по разбору с Хабра: игрок пробирается мимо «вражеского штаба», датчик-камера патрулирует поле, при касании поднимает тревогу, и охранник в течение пяти секунд преследует игрока. Сначала поведение описывается как машина состояний — прямое продолжение схемы из A02. Затем собирается сцена: навигационный меш и компонент NavMeshAgent, три объекта и три скрипта — Player, Guard и Looker, с полным кодом и разбором. Поведение системы можно прочувствовать в интерактивной модели: поуправлять игроком, попасться камере и убегать от охранника.
- Пререквизиты
- A02 — машина состояний; A07 — жизненный цикл, GetComponent; A08 — AddForce, SerializeField; A09 — коллайдеры и события столкновений.
- Мотивация
- Почти любой игре нужен управляемый компьютером персонаж — чаще всего враждебный игроку. В A02 машина состояний врага (патруль → атака → бегство) осталась схемой с заглушками вместо поведения. Пришло время заполнить заглушки: научить противника замечать игрока, догонять его по уровню, огибая препятствия, и прекращать погоню. Всё это делается тремя короткими скриптами и одним встроенным инструментом Unity — навигационным мешем.
1. Поведение как машина состояний
Сформулируем задачу. Игрок должен тайком пробраться мимо вражеского штаба. Поле патрулирует «камера слежения» — плоский красный диск, курсирующий вверх-вниз. Если игрок коснулся камеры — тревога: охранник преследует игрока; через пять секунд после тревоги охранник останавливается, и всё начинается заново.
Прежде чем писать код, нарисуем машину состояний охранника — так, как мы делали это в A02:
Guard стоит · Looker патрулирует
Guard идёт к игроку · таймер 5 с
Состояний всего два, и переключать их можно самым простым способом, какой есть в Unity: включать и выключать скрипт преследования через свойство enabled. Выключенный скрипт не получает Update — охранник «не думает»; включённый — преследует. Роль «стрелок» схемы играют событие столкновения и таймер.
2. Сцена и навигационный меш
Объекты сцены (проект 3D, папки Materials и Scripts — как мы заводили в A05):
| Объект | Из чего и с чем | Роль |
|---|---|---|
| Floor | Plane, масштаб X = 7, Z = 3; свой материал | пол, по которому все перемещаются |
| Wall × 4 | Plane, повёрнутые вертикально по периметру | стены, чтобы игрок не свалился с края |
| Player | Sphere + Rigidbody + тег Player | шар игрока под управлением стрелок |
| Guard | Cube + Rigidbody + NavMeshAgent | охранник-преследователь |
| Looker | Sphere, сплюснутая (масштаб 9 × 0.5 × 9), ярко-красный материал | «глаза» охранника: патрулирующий датчик |
Камеру ставим сверху (Y = 30, наклон X = 80°) — вид на поле сверху. Тег Player назначается в выпадающем списке Tag в верхней части инспектора — по нему код будет отличать игрока от прочих объектов.
Навигационный меш (NavMesh) — заранее просчитанная карта проходимых мест, по которой агенты умеют строить маршрут. Создаётся так: Window → Navigation; выбрать Floor и отметить флажок Navigation Static (пол — часть статичной геометрии для навигации); перейти на вкладку Bake и нажать кнопку Bake. Unity просчитает меш, и по полу можно будет «ходить» программно. Компонент NavMeshAgent на охраннике — это исполнитель: ему задаётся точка назначения, а путь до неё он строит и проходит сам.
3. Три скрипта
3.1. Player: движение силами
using UnityEngine; public class Player : MonoBehaviour { private Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); } void Update() { if (Input.GetKey(KeyCode.UpArrow)) rb.AddForce(Vector3.forward * 20); if (Input.GetKey(KeyCode.DownArrow)) rb.AddForce(Vector3.back * 20); if (Input.GetKey(KeyCode.LeftArrow)) rb.AddForce(Vector3.left * 20); if (Input.GetKey(KeyCode.RightArrow)) rb.AddForce(Vector3.right * 20); } }
Знакомая по A08 схема «сила к твёрдому телу», только источник команд — не кнопки UI, а клавиатура: Input.GetKey возвращает true, пока клавиша зажата. Чтение ввода — в Update, как и положено по A07.
3.2. Guard: преследование по мешу
using UnityEngine; using UnityEngine.AI; // пространство имён навигации public class Guard : MonoBehaviour { [SerializeField] private GameObject player; // кого преследовать; назначается в инспекторе private NavMeshAgent navmesh; void Start() { navmesh = GetComponent<NavMeshAgent>(); } void Update() { // каждый кадр цель маршрута — текущая позиция игрока navmesh.destination = player.transform.position; } }
Весь «интеллект» преследования — одна строка: агенту каждое обновление задаётся точка назначения, равная позиции игрока, а маршрут и обход препятствий NavMeshAgent берёт на себя. Заметьте: скрипт всегда преследует — состояние «ожидание» получится его выключением извне. На объекте Guard этот компонент изначально выключен (снята галочка в инспекторе).
3.3. Looker: датчик, таймер и переключение состояний
using UnityEngine; public class Looker : MonoBehaviour { [SerializeField] private GameObject guard; // охранник; назначается в инспекторе private float reset = 5f; // таймер тревоги, секунды private bool movingDown; void Update() { // патрулирование: движение вперёд-назад по оси Z if (movingDown == false) transform.position -= new Vector3(0, 0, 0.1f); else transform.position += new Vector3(0, 0, 0.1f); if (transform.position.z > 10) movingDown = false; else if (transform.position.z < -10) movingDown = true; // обратный отсчёт тревоги в реальном времени reset -= Time.deltaTime; if (reset < 0) { guard.GetComponent<Guard>().enabled = false; // погоня окончена GetComponent<SphereCollider>().enabled = true; // датчик снова «зрячий» } } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Player")) { guard.GetComponent<Guard>().enabled = true; // тревога: включить погоню reset = 5f; // перезапустить таймер GetComponent<SphereCollider>().enabled = false; // датчик «ослеп», игрок не застрянет } } }
Здесь и происходит переключение машины состояний:
- OnCollisionEnter — событие физического столкновения (набор OnCollision из A07). Проверка CompareTag("Player") отсекает столкновения с чем угодно, кроме игрока — тег и был нужен для этой строки. В исходном разборе использовано сравнение tag == "Player"; CompareTag делает то же, но без лишнего создания строки — привыкайте сразу к нему.
- При тревоге включается скрипт Guard (enabled = true — переход в состояние «преследование»), таймер сбрасывается на 5 секунд, а коллайдер датчика выключается: игрок не застрянет в нём, и тревога не будет подниматься повторно каждое касание.
- reset -= Time.deltaTime — уменьшение таймера на время, прошедшее с прошлого кадра: 5 условных единиц превращаются в честные 5 секунд независимо от частоты кадров (вспомните врезку про Update в A07). Когда таймер истёк, скрипт Guard выключается, коллайдер датчика включается — система вернулась в «ожидание».
Финальная сборка: скрипт Player — на объект Player; скрипт Guard — на объект Guard, в поле player перетащить объект Player, галочку компонента снять; скрипт Looker — на объект Looker, в поле guard перетащить объект Guard. Запускаем: игрок катается стрелками, камера патрулирует; коснулись красного диска — охранник срывается в погоню и через пять секунд отстаёт.
Модель сцены сверху: синий шар — игрок (стрелки клавиатуры или кнопки), красный диск — патрулирующая камера, серый квадрат — охранник. Коснитесь камеры — и следите за состоянием охранника и таймером тревоги. Попробуйте пересечь поле слева направо, не подняв тревогу.
Кликните по полю, чтобы клавиатура работала, — или пользуйтесь кнопками.
Контрольные вопросы
-
Два состояния: «ожидание» (скрипт Guard выключен, Looker патрулирует) и «преследование» (Guard включён, идёт к игроку). Переходы: столкновение игрока с Looker → преследование; истечение пятисекундного таймера → ожидание.
-
NavMesh — заранее запечённая карта проходимых мест уровня (Window → Navigation → Bake по статичной геометрии). NavMeshAgent — компонент-исполнитель: получает точку назначения (navmesh.destination) и сам строит и проходит маршрут с обходом препятствий.
-
Через свойство enabled компонента: скрипт Guard, который всегда преследует, включается при тревоге (enabled = true) и выключается по таймеру (enabled = false). Выключенный скрипт не получает Update — охранник «не думает».
-
Две причины: игрок не застрянет в датчике (столкновения больше не рассчитываются) и тревога не будет подниматься повторно на каждое касание. По истечении таймера коллайдер включается снова — цикл замыкается.
-
Уменьшает таймер на время, прошедшее с прошлого кадра, — отсчёт идёт в реальных секундах. Константа на кадр сделала бы длительность тревоги зависимой от частоты кадров: на быстром компьютере погоня заканчивалась бы быстрее.
-
Результат тот же, но CompareTag не создаёт лишнюю строку при каждом вызове и сообщает об опечатке в имени тега; сравнение строк через == делает и то и другое хуже. В коде, выполняемом при каждом столкновении, это имеет значение.
Источники
- Создание простого ИИ на C# в Unity // Хабр : [сайт]. — URL: https://habr.com/ru/articles/419641/ (дата обращения: 08.07.2026).
- Navigation and Pathfinding // Unity Documentation : [сайт]. — URL: https://docs.unity3d.com/Manual/Navigation.html (дата обращения: 08.07.2026).
- NavMeshAgent // Unity Scripting API : [сайт]. — URL: https://docs.unity3d.com/ScriptReference/AI.NavMeshAgent.html (дата обращения: 08.07.2026).