Поведение NPC: конечные автоматы, деревья поведения, GOAP и NPC под управлением ИИ
Создайте собственное поведение NPC
Как NPC решают, что им делать? Конечные автоматы, деревья поведения, GOAP, утилитарный ИИ и NPC под управлением LLM — с живыми 3D-примерами прямо в браузере.
Поведение NPC — это логика, которая превращает модель, полоску здоровья и коллизионную капсулу в нечто такое, что, кажется, чего-то хочет. Она отвечает на один вопрос в цикле, обычно каждый кадр или по фиксированному тику: учитывая то, что я знаю о мире, что мне делать дальше? Всё остальное на этой странице — конечные автоматы, деревья поведения, GOAP, утилитарный ИИ, намерения под управлением LLM — это разные ответы на этот вопрос, с разными затратами и разными режимами отказа.
Мы создаём NPC, которые работают во вкладке браузера внутри 3D-конструктора игр Neta Studio. Это ограничение формирует советы ниже. Здесь нет выделенного сервера, нет отдельного потока физики и нет бюджета на планировщик, который ищет 40 миллисекунд на каждом тике. Если технику нельзя сделать достаточно дешёвой, чтобы она выполнялась рядом с рендерингом на ноутбуке среднего класса, мы либо упрощаем её, либо убираем с горячего пути. В следующих разделах описано то, что мы реально выпускаем, и где ломается каждый подход.
Краткий ответ: как NPC принимает решение
NPC принимает решение, выполняя функцию выбора по расписанию, считывая небольшой срез состояния мира (расстояние до цели, линия видимости, текущее здоровье, последняя известная позиция) и возвращая действие, которое могут выполнить системы движения и анимации. Расписание важно не меньше логики. Решения на тике от 100 до 250 мс, при движении и столкновениях каждый кадр, стоят крохотную долю от того, что стоит логика решений каждый кадр, и в большинстве случаев для игрока неотличимы.
Четыре классические модели различаются тем, где живёт сложность. Конечный автомат кодирует решения как переходы между именованными состояниями. Дерево поведения кодирует их как иерархию узлов-селекторов и узлов-последовательностей. GOAP кодирует их как цель плюс набор действий с предусловиями и эффектами и ищет план. Утилитарный ИИ кодирует их как функцию оценки по каждому действию-кандидату. NPC под управлением LLM кодируют намерения высокого уровня на естественном языке и передают результат обратно одной из более дешёвых моделей.
На практике вы их комбинируете. Оценщик утилитарности выбирает цель, дерево поведения или конечный автомат её исполняет, навмеш двигает тело, а языковая модель касается только диалога и намерений. Остальная часть страницы разбирает каждый слой по очереди и называет конкретный режим отказа, с которым вы столкнётесь.
Конечные автоматы (FSM)
Конечный автомат — самая дешёвая полезная модель и правильный первый выбор для большинства NPC. Вы задаёте набор состояний — Ожидание, Патруль, Погоня, Атака, Бегство, Оглушение — и набор условий перехода между ними. На каждом тике вы проверяете переходы из текущего состояния и, если один срабатывает, переключаетесь. Затраты пропорциональны числу проверяемых переходов, а оно невелико. Состояние — это одна переменная, что делает его тривиальным для сериализации, отладки и показа в панели редактора.
Режим отказа — колебания. NPC на расстоянии 5,1 метра от игрока переходит из Патруля в Погоню, доходит до 4,9 метра, и симметричный переход отправляет его назад в Патруль. Он бесконечно вибрирует на границе. Решение — гистерезис: входить в Погоню на 5,0 метра и выходить из неё на 6,5 метра. Та же проблема возникает с проверками линии видимости, которые мерцают, когда игрок перемещается за колонной, и применяется то же решение — потребовать, чтобы условие держалось N тиков подряд, или добавить таймер памяти, который удерживает NPC в Погоне две секунды после последнего обнаружения.
Второй режим отказа — запутанность переходов. Пяти состояниям нужно десять переходов; десяти состояниям нужно уже около сорока пяти. Каждое новое состояние нужно связать с каждым состоянием, из которого в него можно попасть, а условия перекрываются так, что рассуждать о них трудно. Когда таблица переходов перестаёт помещаться на одном экране, это сигнал переходить к дереву поведения.
Конкретный FSM для патрульного охранника: Ожидание (ждать 2 с) → Патруль (идти к следующей точке маршрута) → Тревога (повернуться к шуму, 1,5 с) → Погоня (двигаться к последней известной позиции бегом) → Атака (если в пределах 2 м и перезарядка истекла) → Поиск (блуждать рядом с последней известной позицией 8 с) → Патруль. Семь состояний, примерно дюжина переходов, и именно таймер памяти в Поиске делает поведение осмысленным, а не дёрганым.
- Состояние в одной переменнойТекущее состояние — это одно перечисление. Отладка сводится к чтению одного значения, а не к восстановлению намерения по стеку.
- Гистерезис на каждом порогеЛюбое расстояние или таймер, которые могут переключаться в обе стороны, требуют разных значений входа и выхода, иначе NPC колеблется на границе.
- Бюджет переходовДержите число состояний примерно ниже десяти, пока таблица не станет необслуживаемой. После этого переходите к дереву поведения или разделяйте на подмашины.
Деревья поведения
Дерево поведения заменяет таблицу переходов иерархией. Листовые узлы — это действия (MoveTo, PlayAnimation, Wait) и условия (IsPlayerVisible, HealthBelow). Основную работу выполняют два типа составных узлов. Селектор пробует своих детей по порядку и возвращает успех, как только один из них добился успеха, — это ваш список приоритетов. Последовательность выполняет своих детей по порядку и терпит неудачу, как только один из них не выполнился, — это ваша пошаговая процедура.
Выигрыш в масштабировании — это композируемость. Добавить новое поведение означает добавить поддерево под селектор, а не связать новое состояние с каждым состоянием, из которого в него можно попасть. Приоритеты явно заданы порядком детей, и это читается куда легче, чем набор перекрывающихся булевых условий. Поддеревья можно переиспользовать: одна и та же ветвь «исследовать шум» работает для охранника, собаки и мирного жителя.
Деревья не становятся автоматически быстрее. Наивная реализация заново оценивает всё дерево при каждом тике, поэтому тикать нужно по расписанию решений в 100–250 мс, а текущее выполняющееся действие должно управлять движением каждый кадр. Долго выполняющимся действиям нужно состояние выполнения, которое дерево запоминает, иначе действие начинается заново на каждом тике и NPC съезжает к началу своего пути. Большинство движков, поставляющих дерево поведения — пакет Behavior для Unity, встроенное дерево Unreal или небольшое собственное на TypeScript, — обрабатывают это возвращаемым значением Running, но именно это стоит проверить в первую очередь, когда NPC дёргается.
Структура селектора для боевого NPC, по порядку приоритета: (1) если здоровье ниже 25 процентов, последовательность [Отойти в укрытие, Лечиться]; (2) если игрок виден и в пределах 2 м, последовательность [Повернуться к игроку, Атаковать]; (3) если игрок виден, последовательность [Переместиться на позицию для стрейфа, Атаковать]; (4) если задана последняя известная позиция, Искать; (5) Патрулировать. Селектор выбирает первую ветвь, чьё условие проходит. Перестановка этих детей — это способ настроить агрессивность, не трогая код.
- Узел-селекторВозвращает успех на первом ребёнке, который завершился успешно. Используйте его для приоритетов по нисходящей: сначала самое специфичное поведение, затем следующее, затем поведение по умолчанию.
- Узел-последовательностьТерпит неудачу на первом ребёнке, который не выполнился. Используйте его для упорядоченных процедур, где второй шаг бессмыслен, если не произошёл первый.
- Состояние выполненияДолгие действия должны возвращать Running и продолжаться с места остановки. Если они сразу возвращают Success, дерево перезапускает их каждый тик и движение ломается.
- БлэкбордОбщее хранилище «ключ — значение» для фактов, которые дерево читает и записывает: позиция цели, уровень тревоги, окончание перезарядки. Держите его небольшим и типизированным.
GOAP (планирование действий, ориентированное на цели)
GOAP переворачивает модель. Вместо описания того, когда совершать переход, вы описываете, что делают действия и чего они требуют, а планировщик ищет последовательность, ведущую к цели. У каждого действия есть предусловия, эффекты и стоимость. «Attack» требует HasWeapon и InRange и задаёт TargetDead. «GetWeapon» требует WeaponAvailable и задаёт HasWeapon. «MoveToTarget» требует TargetKnown и задаёт InRange. Планировщик начинает с цели, работает в обратном направлении и находит самую дешёвую цепочку действий, чьи эффекты её удовлетворяют.
Это окупается, когда состояние мира богато, а правильная последовательность неочевидна. NPC, которому нужно разорвать линию видимости, перезарядиться, сменить позицию и затем вступить в бой, порождает план, который дизайнер никогда явно не писал, потому что планировщик обнаружил порядок из графа предусловий. Это окупается и в производстве, ведь добавление нового предмета или способности означает добавление одного определения действия, а не правку каждого конечного автомата, который должен о нём знать.
Плата за это — поиск. Наивный планировщик перебирает комбинации действий, пока не найдёт план, что может взорваться по мере добавления действий и переменных состояния. Стандартная мера — эвристика, A* по графу состояний с оценкой расстояния до цели, плюс строгий лимит раскрытия. В наших собственных браузерных контекстах планы вычисляются на тике принятия решений с ограничением в несколько сотен раскрытий узлов, и всё, что его превышает, откатывается к закэшированному плану или простой ветви дерева поведения. Честный предел таков: GOAP оправдывает затраты на поиск, когда число осмысленных последовательностей действий велико, и оказывается потраченными впустую накладными расходами, когда NPC всегда делает лишь три вещи.
- Предусловия и эффектыКаждое действие объявляет, что должно быть истинно для его запуска и что оно меняет. Планировщик рассуждает над этими объявлениями, а не над кодом.
- Поиск назад от целиНачните с желаемого состояния и найдите действия, чьи эффекты его создают, затем рекурсивно перейдите к их предусловиям. Обычно дешевле прямого поиска.
- Лимит раскрытияОграничьте число раскрытий узлов на вызов планировщика. Превысили лимит — откатывайтесь: никогда не позволяйте планировщику задерживать кадр.
- Кэширование плановПерепланируйте только при значимом изменении состояния мира. В статичных ситуациях прежний план используется бесплатно.
Утилитарный ИИ
Утилитарный ИИ оценивает каждый вариант и выбирает наивысший. У каждого действия-кандидата есть функция, отображающая состояние мира в число, обычно нормализованное от нуля до единицы. «Бегство» оценивается высоко, когда здоровье низкое, а враги рядом. «Атака» оценивается высоко, когда цель в пределах досягаемости, а боеприпасы выше нуля. «Блуждание» оценивается умеренно, когда ничего другого не давит. NPC берёт argmax, возможно, с небольшим случайным разбросом, чтобы равенства разрешались непредсказуемо.
Это модель, порождающая персонажей, которые выглядят наиболее неожиданно, потому что поведение возникает из взаимодействия кривых, а не из явных правил. Два NPC с немного разными весами оценок ведут себя заметно по-разному без какого-либо ветвящегося кода, что делает эту модель естественным выбором для вариаций характера. Это ещё и самая простая в настройке на лету модель: выведите веса в отладочную панель и перетаскивайте их, пока игра работает.
Режим отказа — плоская или шумная полезность. Если каждое действие получает 0,4, NPC выглядит нерешительным и постоянно переключается. Если высшая оценка скачет каждый тик между двумя близкими значениями, вы получаете те же колебания, что и при плохом конечном автомате. Стандартное решение — окно обязательства: раз действие выбрано, удерживайте его минимально фиксированное время — 500 мс разумное значение по умолчанию, — если только высшая оценка не превышает оценку выбранного действия на заметную величину. Кривые тоже важны. Линейные кривые отклика по здоровью дают NPC, который убегает точно в один и тот же момент в каждом бою. Экспоненциальные или логистические кривые дают пороги, которые меняются вместе с ситуацией.
- Функции оценки для каждого действияКаждое действие отображает состояние мира в полезность от нуля до единицы. Держите их независимыми, чтобы можно было перенастроить одну, не трогая остальные.
- Окно обязательстваВыбрав действие, удерживайте его минимальную длительность. Это предотвращает переключения каждый тик между почти равными оценками.
- Настраиваемые весаВыводите веса вживую. Два NPC, отличающиеся только весами, воспринимаются как разные персонажи — это самая дешёвая система характера из доступных.
- Кривые откликаЛинейные кривые создают фиксированные пороги. Логистические кривые заставляют NPC реагировать на ситуацию, а не на магическое число.
Навигация и столкновения
Логика решений выбирает пункт назначения. Навигация доставляет туда тело. Навмеш — это триангулированная проходимая поверхность; поиск пути выполняет A* по графу её полигонов, а результат — список рёбер полигонов, по которым движется NPC. Мы строим свой при загрузке из геометрии уровня и кэшируем его, потому что перестроение каждый кадр — самый простой способ убить частоту кадров в браузерной игре.
Рулевое поведение отвечает за последние несколько метров и за движение в каждый момент. Поведения преследования и прибытия формируют вектор движения вперёд, разделение отталкивает NPC от близких агентов, а обход препятствий изгибает путь вокруг динамических блокираторов вроде двери или другого персонажа. Запускайте рулевое поведение каждый кадр; запускайте поиск пути только когда пункт назначения меняется или путь становится недействительным. Путь, пересчитываемый каждый кадр для каждого NPC, — распространённая и устранимая трата.
Столкновения — это проход, который не даёт NPC пройти сквозь стену. Навмеш удерживает путь на проходимой земле, но рулевое поведение и физика всё ещё могут втолкнуть персонажа в геометрию, поэтому контроллер персонажа разрешает проникновение относительно коллайдеров после движения. В наших изометрических мирах — Mini World и The Cyclops' Island — именно отсюда берётся большинство видимых багов NPC: путь, проходящий через щель уже радиуса персонажа, или вектор рулевого поведения, вталкивающий агента в стену на углу. Правильная установка радиуса агента на навмеше и удержание силы разделения ниже силы выталкивания контроллера устраняют большую часть этого.
01
Запеките навмеш при загрузке из статической геометрии и закэшируйте его. Не перестраивайте его каждый кадр.
02
Задайте радиус и высоту агента по фактической коллизионной капсуле персонажа, а не по ограничивающему прямоугольнику модели.
03
Запускайте поиск пути только когда меняется пункт назначения, путь заблокирован или NPC отклонился от пути дальше порога.
04
Запускайте рулевое поведение, разделение и обход препятствий каждый кадр с ограниченной силой разделения.
05
Разрешайте столкновения с коллайдерами после движения, затем возвращайте персонажа на навмеш.
06
Логируйте и визуализируйте путь и навмеш на отладочной панели — баги обычно пространственные, а не логические.
NPC под управлением LLM
Языковая модель меняет картину на уровне намерений и диалога, а не навигации. Поиск пути, рулевое поведение и столкновения остаются точно такими же, потому что модель, отвечающая сотни миллисекунд, не может управлять телом, которому нужно двигаться каждый кадр. Что модель может делать — решать, какая цель имеет смысл с учётом слов игрока, генерировать текст ответа в образе персонажа и выбирать между вариантами высокого уровня, когда ситуация неоднозначна, а написанные вручную правила дали бы что-то плоское.
Работоспособная архитектура разделяет две скорости. Немедленные реакции идут на конечном автомате или оценщике утилитарности — атаковать, убегать, укрываться, — потому что они должны происходить в течение кадра-двух. Продуманное намерение проходит через модель с куда более медленной частотой, возможно раз за взаимодействие или каждые несколько секунд, и его вывод — это структурированная команда, которую исполняет быстрый слой: задать цель «Следовать», задать отношение к игроку «Враждебное», задать реплику диалога. Модель никогда не трогает позицию напрямую.
Честные пределы — это задержка, стоимость и детерминизм. Время отклика достаточно велико, чтобы NPC должен был немедленно подтвердить ввод жестом или короткой заготовленной репликой, иначе он выглядит сломанным. Стоимость растёт с частотой вызовов, поэтому вы группируете и кэшируете. А поскольку вывод недетерминирован, вы должны проверять его по схеме, прежде чем он дойдёт до игры: ответ свободным текстом, который не разбирается, должен откатываться к реплике по умолчанию, а не ронять систему диалога. Относитесь к модели как к ненадёжному сервису за типизированным интерфейсом — и интеграция выдержит.
- Типизированный вывод командМодель возвращает структурированное намерение, а не прозу, которую разбирает игровой код. Проверяйте по схеме и отклоняйте всё некорректное.
- Двухскоростной циклБыстрая локальная логика для реакций, медленные вызовы модели для обдумывания. Никогда не ставьте сетевой обмен на путь движения.
- Немедленное подтверждениеПроиграйте жест или короткую заготовленную реплику сразу при поступлении ввода, затем замените её, когда ответит модель. Скрывает задержку.
- Плавный откатЕсли вызов не удался или истёк тайм-аут, откатывайтесь к ответу конечного автомата по умолчанию. NPC никогда не должен зависать в ожидании модели.
Войдите в мир, которому он принадлежит
Спрайт — это персонаж, которому негде стоять. Последний шаг — тот, которого не предлагает больше никто: возьмите лист, который вы только что создали, и поместите его в мир, по которому можно ходить, — маленький остров, планету, дождливый уголок улицы. Каждый мир на этом сайте был создан из одного письменного описания и опубликован как страница, в которую можно играть.
Назад к 3D-конструктору игр
Все 31 готовых 3D-мира, игры и сцены — на одной странице, и в каждый можно играть.