Искусственный интеллект в играх прошел долгий путь эволюции. От простейших алгоритмов, движущих призраками в Pac-Man, до сложных систем, управляющих поведением персонажей в современных AAA-проектах. Последним значительным шагом в этой эволюции стал выпуск Unity Technologies пакета Behavior Graph, который стал ответом на растущие потребности разработчиков в гибких и мощных инструментах для реализации игрового AI.
Еще совсем недавно разработчикам приходилось выбирать между покупкой готовых решений в Asset Store или созданием собственных систем для поведенческих графов. Сегодня Behavior Graph заполняет этот пробел в экосистеме Unity, предлагая нативное решение, полностью интегрированное с движком.
Оптимизация игрового AI с помощью Behavior Graph в Unity: практическое руководство
Первые подходы к созданию игрового AI были просты и линейны. Конечные автоматы (Finite State Machines, FSM) с их предсказуемыми переходами между состояниями долгое время были стандартом индустрии. Затем появились деревья поведения (Behavior Trees), позволившие создавать более сложные и разветвленные модели принятия решений. Они стали настоящим прорывом, позволяя разработчикам выстраивать иерархические структуры поведений, где каждый узел мог возвращать успех, провал или продолжающийся процесс.
Behavior Graph расширяет эту концепцию, превращая древовидную структуру в полноценный граф, где поведенческие паттерны могут сливаться, разделяться и взаимодействовать более сложными способами. Это открывает новые горизонты для игрового AI, делая его более адаптивным и реалистичным.
Сравнивая Behavior Graph с другими современными подходами, стоит отметить несколько ключевых различий:
- В отличие от систем планирования, ориентированных на цели (GOAP), Behavior Graph не требует сложных вычислений для построения цепочек действий на каждом шаге. Вместо этого он предлагает визуальный подход к проектированию поведения, что упрощает отладку и итерационное улучшение.
- По сравнению с Utility AI, которые используют числовые оценки для выбора оптимальных действий, Behavior Graph предлагает более структурированный подход, сохраняя при этом гибкость благодаря возможности ветвления на основе условий и событий.
- Для различных жанров игр Behavior Graph предлагает свои преимущества. В экшн-играх он позволяет создавать врагов с разнообразными тактиками атаки и защиты. В стратегиях он упрощает координацию множества юнитов. В RPG помогает создавать живой мир, где NPC реагируют на изменения окружения и действия игрока естественным образом.
- Концептуально Behavior Graph представляет собой визуальный инструмент для создания логики AI, где узлы содержат действия или условия, а связи между ними определяют потоки выполнения. В отличие от чистого кода, такой подход делает AI более наглядным и позволяет быстрее вносить изменения без глубокого погружения в программирование.
Behavior Graph в Unity — не просто инструмент, а новая парадигма в разработке игрового AI, объединяющая лучшие практики классических подходов с современными потребностями игровой индустрии. В следующих разделах мы погрузимся в технические аспекты и практическое применение этой технологии, научимся создавать сложные поведенческие паттерны и оптимизировать их работу в реальных проектах.
Технические основы
Графоориентированная модель данных лежит в основе всей архитектуры Behavior Graph. В отличие от линейных алгоритмов или простых конечных автоматов, граф представляет собой структуру из узлов (nodes) и рёбер (edges), где каждый узел содержит определённую логику, а рёбра указывают на возможные пути выполнения этой логики. Такая структура позволяет создавать сложные взаимосвязи между действиями и условиями, что значительно расширяет возможности AI в играх. Архитектура Behavior Graph в Unity построена на концепции потока управления (flow control), где каждый узел возвращает определённый статус после выполнения. В классических деревьях поведения узлы обычно возвращают три статуса: Success (успех), Failure (провал) и Running (выполнение продолжается). Behavior Graph расширяет эту модель, добавляя статусы Waiting (ожидание) и Initializing (инициализация), что даёт более тонкий контроль над процессом выполнения.
Ключевое отличие Behavior Graph от традиционных деревьев поведения заключается в том, что граф не ограничен строгой иерархической структурой. В деревьях поведения узлы всегда организованы в древовидную структуру, где каждый дочерний узел имеет только одного родителя. В Behavior Graph же узлы могут соединяться более свободно, образуя циклы, ветвления и слияния путей выполнения, что делает систему гораздо более гибкой. Среди ключевых компонентов Behavior Graph выделяются несколько типов узлов:
1. Action nodes (узлы действий) — выполняют конкретные операции, например, перемещение персонажа или атаку. Они являются "листьями" графа, то есть узлами, которые непосредственно взаимодействуют с игровым миром.
2. Sequencing nodes (узлы последовательностей) — аналогичны композитным узлам в деревьях поведения, управляют порядком выполнения дочерних узлов. Могут выполнять узлы последовательно или параллельно.
3. Modifier nodes (узлы-модификаторы) — изменяют результат дочернего узла. Например, Inverter меняет успех на провал и наоборот, что позволяет переиспользовать логику для противоположных условий.
4. Conditional nodes (условные узлы) — осуществляют ветвление на основе условий, аналогично операторам if/else в коде.
5. Join nodes (узлы соединения) — позволяют объединять разные ветви графа, превращая дерево в настоящий граф.
6. Event nodes (узлы событий) — отправляют и принимают события, позволяя разным частям графа или даже разным графам взаимодействовать между собой.
Соединения между узлами представляют собой направленные связи, указывающие, какой узел будет выполнен следующим в зависимости от результата текущего узла. Например, если условный узел возвращает True, выполнение может пойти по одной ветке, а если False — по другой. Это напоминает операторы ветвления в традиционном программировании, но с визуальным представлением и возможностью динамически изменять поток выполнения. Состояния в Behavior Graph управляются через систему "блэкборда" (blackboard) — хранилища переменных, доступных для всех узлов графа. Blackboard содержит все данные, необходимые для принятия решений и выполнения действий: позиции объектов, состояния персонажей, цели и приоритеты. Каждая переменная в блэкборде имеет тип, значение и два дополнительных булевых свойства: "Exposed" (делает переменную видимой в инспекторе Unity) и "Shared" (делает переменную доступной для всех экземпляров данного графа). Благодаря блэкборду реализуется принцип разделения логики и представления. Граф определяет абстрактную логику поведения (например, "перемещение к цели"), а конкретные параметры этого поведения (скорость, точка назначения, анимации) задаются через переменные в блэкборде. Это позволяет переиспользовать один и тот же граф для разных персонажей, меняя только параметры, без необходимости дублировать логику.
Важной технической особенностью Behavior Graph является то, что он не просто выполняет узлы за один кадр. Узлы могут выполняться на протяжении нескольких кадров или даже минут игрового времени, возвращая статус Running, пока действие не будет завершено. Это позволяет создавать сложные поведенческие последовательности, занимающие протяжённое время, без необходимости писать сложный код для управления временными интервалами.
Рассмотрим более детально узлы и соединения как базовые строительные блоки Behavior Graph. Каждый узел в графе — функциональная единица с чётко определённой задачей и выходным статусом. Узлы действий (Action nodes) могут сильно различаться по функциональности: от простых проверок значений переменных до сложных алгоритмов поиска пути или принятия тактических решений. Интересная особенность Action nodes в Unity Behavior Graph — возможность иметь один дочерний узел. Обычно в классических деревьях поведения листовые узлы не имеют дочерних элементов. В Behavior Graph же действия могут выстраиваться в вертикальные цепочки, где каждое действие остаётся независимым в исполнении, но визуально они организованы в компактную группу для лучшей читаемости графа. Conditional nodes (условные узлы) работают схожим образом с обычными условными операторами в программировании, но с важным отличием — они могут обрабатывать множественные условия одновременно. В инспекторе узла можно настроить, требуется ли для ветвления истинность всех условий (логическое И) или достаточно хотя бы одного (логическое ИЛИ). Эта возможность устраняет необходимость создавать отдельные условные узлы для каждой проверки, делая граф компактнее и понятнее. Repeat nodes (узлы повторения) представляют собой специализированный тип узлов, который выполняет повторное исполнение дочерней ветви до тех пор, пока заданное условие не будет выполнено. Это позволяет создавать циклические паттерны поведения — например, патрулирование по точкам или многократные попытки выполнить какое-либо действие до достижения успеха.
Для эффективного управления состояниями Behavior Graph предлагает не только систему блэкборда, но и мощный механизм событий. Event nodes позволяют частям графа реагировать на происходящее в игре асинхронно, без необходимости постоянно проверять условия. Например, вместо того чтобы каждый кадр проверять, вошёл ли игрок в зону обнаружения, можно настроить событие, которое активирует соответствующую ветвь графа только когда это фактически произойдёт. События в Behavior Graph могут быть как внутренними (между узлами одного графа), так и внешними (между разными графами или даже между графом и внешним кодом). Для внешней коммуникации используются специальные ScriptableObject-каналы событий (Event Channels), которые создаются в проекте и назначаются соответствующим узлам отправки и приёма событий. Механизм событий тесно связан с концепцией субграфов (Subgraphs) — переиспользуемых фрагментов поведения, которые можно включать в другие графы как единое целое. Субграфы функционируют аналогично подпрограммам в традиционном программировании: они инкапсулируют определённую логику и могут принимать параметры через блэкборд. Это значительно уменьшает дублирование кода и упрощает поддержку сложных поведенческих систем.
Разделение логики и представления — фундаментальный принцип работы с Behavior Graph. Граф определяет общую логику поведения на высоком уровне абстракции, в то время как конкретные детали реализации (анимации, звуки, физические движения) выносятся в отдельные компоненты или параметры. Такой подход обеспечивает гибкость и масштабируемость AI-систем. На практике это разделение реализуется через:
1. Компонент Behavior Agent, который назначается игровому объекту и связывает конкретный экземпляр объекта с абстрактным графом поведения.
2. Экспозицию переменных блэкборда в инспекторе, что позволяет настраивать конкретные параметры поведения без изменения самого графа.
3. Создание пользовательских узлов действий, которые реализуют специфическую функциональность, но абстрагированы от конкретного контекста использования.
Инспектор графа поведения — мощный инструмент для настройки параметров узлов и визуализации их состояния. Каждый выбранный узел отображает свои настраиваемые параметры в инспекторе, что позволяет быстро менять его поведение без необходимости изменять код. Для условных узлов инспектор даёт возможность создавать сложные условные выражения, что устраняет необходимость программирования на низком уровне. Интеграция пользовательского кода с Behavior Graph осуществляется через механизм пользовательских узлов. Unity Behavior предоставляет API для создания собственных типов узлов с произвольной логикой. Процесс создания пользовательского узла включает несколько шагов:
1. Определение имени, категории и описания узла с помощью атрибутов.
2. Указание переменных, которые будут доступны для настройки в инспекторе.
3. Реализация методов OnStart(), OnUpdate() и других для определения логики узла.
После создания пользовательского узла он становится доступен в палитре узлов Behavior Graph и может использоваться так же, как и встроенные узлы. Это обеспечивает бесшовную интеграцию специализированной логики игры с общей системой поведения.
Важно понимать, что Behavior Graph не является визуальным скриптингом в традиционном понимании. Его цель — не замена программирования, а создание высокоуровневой абстракции для управления поведением AI. В то время как визуальное скриптирование фокусируется на замене программирования визуальными элементами, Behavior Graph предоставляет структурированный способ моделирования поведенческих паттернов, оставляя низкоуровневую реализацию действий на уровне кода. Такой подход позволяет достичь баланса между наглядностью визуального представления и производительностью кодовой реализации конкретных алгоритмов. Программисты могут сосредоточиться на создании эффективных узлов действий, а дизайнеры — на конструировании высокоуровневой логики поведения без погружения в технические детали реализации.
Практическая реализация
Переход от теории к практике начинается с установки пакета Unity Behavior через Package Manager. На момент написания статьи, пакет находится в версии 1.0.3 и доступен через официальный реестр пакетов Unity. После установки в проекте появляются новые инструменты для создания и редактирования поведенческих графов. Создание первого графа поведения начинается с щелчка правой кнопкой мыши в Project Window и выбора Create → Behavior → Graph. После создания файла графа двойной щелчок по нему открывает редактор Behavior Graph — специализированное окно с панелью инструментов, рабочей областью и инспектором узлов.
Базовая структура графа всегда начинается с узла Start, который автоматически исполняется при запуске графа. К нему можно подключать другие узлы, формируя логику поведения. Например, простейший граф для патрулирования персонажа может выглядеть так:
1. Start → Sequence → Repeat (для зацикливания патрулирования).
2. Repeat → GetNextPatrolPoint → MoveToTarget → WaitTime.
Эта простая схема уже создаёт функциональное поведение, где персонаж циклически перемещается между точками патрулирования с паузами. Для улучшения поведения можно добавить условные узлы, например, проверяющие обнаружение игрока или получение урона, и соответствующие реакции. Для взаимодействия графа с конкретным игровым объектом используется компонент Behavior Agent. Этот компонент добавляется к объекту через меню Component → Behavior → Agent и принимает файл графа как параметр. Все переменные графа, помеченные как "Exposed", становятся доступны для настройки в инспекторе:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
| // Пример доступа к переменным Behavior Graph из скрипта
[SerializeField] private BehaviorAgent _agent;
void Start()
{
// Получение доступа к переменной блэкборда
_agent.SetBlackboardValue<float>("PatrolSpeed", 3.5f);
// Запуск графа
_agent.Start();
} |
|
Механика работы Behavior Graph основана на потоковом исполнении узлов. Каждый узел может находиться в одном из нескольких состояний: Success, Failure, Running, Waiting или Initializing. Эти состояния определяют, как будет продолжаться выполнение графа. Создание пользовательских узлов значительно расширяет возможности графа. Например, можно создать узел для 2D-движения персонажа, если встроенные узлы навигации не подходят для вашего проекта:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| [Serializable, GeneratePropertyBag]
[NodeDescription(name: "Move2D",
story: "[Agent] Navigates To [Target]",
category: "Action",
id: "739a0711c5da6e4ca560ae951d045b61")]
public partial class Move2DAction : Action
{
[SerializeReference] public BlackboardVariable<GameObject> Agent;
[SerializeReference] public BlackboardVariable<GameObject> Target;
[SerializeReference] public BlackboardVariable<float> Speed = new(1.0f);
[SerializeReference] public BlackboardVariable<float> DistanceThreshold = new(0.2f);
private Rigidbody2D _agentRb;
protected override Status OnStart()
{
if (Agent?.Value == null || Target?.Value == null)
return Status.Failure;
_agentRb = Agent.Value.GetComponent<Rigidbody2D>();
if (_agentRb == null)
return Status.Failure;
return Status.Running;
}
protected override Status OnUpdate()
{
if (Agent?.Value == null || Target?.Value == null)
return Status.Failure;
Vector2 direction = (Target.Value.transform.position - Agent.Value.transform.position);
float distance = direction.magnitude;
if (distance <= DistanceThreshold.Value)
return Status.Success;
direction.Normalize();
_agentRb.velocity = direction * Speed.Value;
return Status.Running;
}
} |
|
Этот пример демонстрирует, как создать узел, который перемещает объект с компонентом Rigidbody2D к указанной цели. Метод OnStart вызывается при первом выполнении узла, а OnUpdate — на каждом последующем кадре, пока узел находится в состоянии Running. Отладка и визуализация работы графа — одно из главных преимуществ этого подхода. В режиме воспроизведения редактор Behavior Graph показывает, какие узлы активны, какие выполнены успешно, а какие завершились неудачей. Это даёт наглядное представление о работе AI и упрощает выявление проблем.
Для углубленной отладки можно использовать точки останова (breakpoints), которые добавляются к узлам через контекстное меню. Когда выполнение достигает узла с точкой останова, игра приостанавливается, и соответствующий код открывается в IDE, позволяя изучить текущее состояние переменных.
Работа с событиями в Behavior Graph осуществляется через специализированные узлы:
1. Start On Event — запускает выполнение ветви графа при получении события.
2. Wait For Event — приостанавливает выполнение до получения события.
3. Send Event — отправляет событие.
Для коммуникации между графами используются специальные ScriptableObject-ы типа EventChannel. Они создаются через меню Create → Behavior → Event Channels и служат "мостами" для передачи сигналов между разными частями игры.
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
| // Пример отправки события из кода
[SerializeField] private EventChannel _enemySpottedChannel;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
_enemySpottedChannel.Invoke();
}
} |
|
Такой подход позволяет создавать слабосвязанные системы, где компоненты взаимодействуют друг с другом через события, не зная о конкретной реализации друг друга.
Одним из мощных приёмов в работе с Behavior Graph является использование субграфов (Subgraphs). Субграфы — это переиспользуемые фрагменты поведения, которые можно включать в другие графы через узел Run Subgraph. Они особенно полезны для поведений, которые повторяются у разных типов персонажей. Например, механика "убегания от опасности" может быть вынесена в отдельный субграф и переиспользована для разных врагов, гражданских NPC и даже животных в игре. При этом конкретные параметры (скорость бегства, дистанция обнаружения, анимации) могут настраиваться индивидуально через переменные блэкборда. Важной частью работы с субграфами является передача параметров через экспозицию переменных в блэкборде. Переменные, отмеченные как "Exposed", становятся доступными для настройки в инспекторе узла Run Subgraph. Это позволяет настраивать поведение субграфа индивидуально для каждого случая использования:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
| // Пример получения экспозированных переменных внутри субграфа
protected override Status OnUpdate()
{
// Переменная Speed экспозирована и может иметь разные значения
// в разных местах использования субграфа
float adjustedSpeed = Speed.Value * SpeedMultiplier.Value;
// Остальная логика узла...
_rb.velocity = _direction * adjustedSpeed;
return Status.Running;
} |
|
Ещё одним преимуществом субграфов является возможность их замены во время выполнения. Вы можете иметь несколько альтернативных субграфов для одного и того же сценария и выбирать между ними динамически:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Выбор субграфа в зависимости от сложности игры
[SerializeField] private BehaviorGraph _easyEnemyBehavior;
[SerializeField] private BehaviorGraph _mediumEnemyBehavior;
[SerializeField] private BehaviorGraph _hardEnemyBehavior;
void UpdateDifficulty(GameDifficulty difficulty)
{
BehaviorGraph selectedGraph = difficulty switch
{
GameDifficulty.Easy => _easyEnemyBehavior,
GameDifficulty.Medium => _mediumEnemyBehavior,
GameDifficulty.Hard => _hardEnemyBehavior,
_ => _mediumEnemyBehavior
};
// Находим все узлы RunSubgraph и меняем их граф
var runSubgraphNodes = _mainBehaviorGraph.GetNodes<RunSubgraphNode>();
foreach (var node in runSubgraphNodes.Where(n => n.GetBlackboardValue<string>("BehaviorType") == "Combat"))
{
node.SetSubgraph(selectedGraph);
}
} |
|
Интеграция Behavior Graph с системами Unity ECS (Entity Component System) представляет собой интересный вариант объединения высокоуровневой логики поведения с высокопроизводительной архитектурой данных. Хотя Behavior Graph сам по себе не является ECS-системой, он может взаимодействовать с ней через специальные узлы или пользовательские адаптеры.
Один из подходов — использование Behavior Graph для высокоуровневого принятия решений, а ECS для эффективного выполнения однотипных операций. Например граф может определить, что отряд должен атаковать противника, а ECS-система займётся эффективной обработкой анимаций, эффектов и физики для множества юнитов.
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Пример узла, который отправляет команду в ECS-систему
public class SendECSCommandNode : Action
{
[SerializeReference] public BlackboardVariable<CommandType> CommandType;
[SerializeReference] public BlackboardVariable<Entity> TargetEntity;
protected override Status OnUpdate()
{
var command = new UnitCommand
{
Type = CommandType.Value,
Target = TargetEntity.Value
};
// Добавление команды в очередь для обработки ECS-системой
World.DefaultGameObjectInjectionWorld.EntityManager
.GetComponentData<CommandBuffer>(CommandBufferEntity)
.AddCommand(command);
return Status.Success;
}
} |
|
Углубляясь в отладку и визуализацию работы графа, стоит отметить специальный режим Debug в редакторе Behavior Graph. При активации этого режима каждый узел отображает иконку своего текущего состояния: зелёная галочка для успешно выполненных, красный крест для ошибок, синий значок для выполняющихся узлов и т.д. Это создаёт мощный визуальный инструмент, позволяющий наблюдать за потоком выполнения AI в реальном времени. Для более детальной отладки конкретных узлов применяются точки останова. При добавлении точки останова к узлу (через контекстное меню "Toggle Breakpoint") выполнение игры будет приостановлено, когда поток управления достигнет этого узла. Это открывает соответствующий код в IDE с активной сессией отладки, позволяя изучать значения переменных и состояние объектов в критических точках.
Для сложных графов рекомендуется использовать прозрачную организацию с помощью заметок (sticky notes) и группировки узлов. Заметки создаются через контекстное меню в редакторе графа и служат документацией, объясняющей логику работы отдельных частей:
C#
Скопировано | 1
2
3
4
5
| [Sticky Note]
Эта секция графа отвечает за поведение персонажа в бою.
1. Сначала проверяется здоровье — если оно низкое, персонаж попытается найти аптечку.
2. Если здоровье в норме, проверяется наличие противников в зоне видимости.
3. Для врагов в зоне досягаемости происходит атака, для дальних — перемещение к ним. |
|
Коммуникация между агентами через Behavior Graph реализуется двумя основными способами: через события и через общие переменные блэкборда. Событийная модель позволяет агентам обмениваться сигналами, не зная друг о друге напрямую. Например, лидер отряда может отправить событие "Отступать!" через глобальный канал событий, и все члены отряда, слушающие этот канал, получат сигнал к отступлению:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Создание канала команд в редакторе
[CreateAssetMenu(fileName = "SquadCommandChannel", menuName = "Game/Channels/Squad Command")]
public class SquadCommandChannel : EventChannel
{
// События могут нести полезную нагрузку
public enum CommandType { Attack, Retreat, Hold, Formation }
public CommandType CurrentCommand { get; private set; }
public void SendCommand(CommandType command)
{
CurrentCommand = command;
Invoke(); // Вызывает событие
}
} |
|
Общие переменные блэкборда (с установленным флагом "Shared") позволяют разным экземплярам одного графа иметь доступ к одним и тем же данным. Это полезно для реализации коллективного знания — например, все члены отряда могут знать о последнем замеченном местоположении игрока, даже если не все его видели:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // В графе поведения члена отряда
[SerializeReference] public BlackboardVariable<Vector3> LastKnownPlayerPosition = new(Vector3.zero, isShared: true);
[SerializeReference] public BlackboardVariable<float> LastUpdateTime = new(0f, isShared: true);
// Когда один из агентов видит игрока, он обновляет общие данные
protected override Status OnUpdate()
{
if (CanSeePlayer.Value)
{
LastKnownPlayerPosition.Value = PlayerTransform.Value.position;
LastUpdateTime.Value = Time.time;
return Status.Success;
}
return Status.Failure;
} |
|
Инкапсуляция и повторное использование поведенческих паттернов являются ключевыми практиками для создания поддерживаемых AI-систем с помощью Behavior Graph. Помимо уже упомянутых субграфов, существуют и другие приёмы:
1. Шаблоны графов — создание "заготовок" графов для типичных сценариев (патрулирование, преследование, атака) с последующей их настройкой для конкретных персонажей.
2. Абстрактные узлы действий — создание узлов с общим интерфейсом, но разными реализациями. Например, узел "Атака" может иметь разные реализации для воина, лучника или мага, но использоваться в графе одинаково.
3. Пакеты поведения — группировка связанных графов, субграфов и узлов в пакеты, которые можно импортировать в разные проекты или передавать между командой.
Одной из продвинутых практик является создание "библиотек поведения" — наборов готовых графов и узлов для типичных игровых жанров. Например, библиотека для шутера может включать графы для разных типов врагов, систему алертов и проверки зон видимости, модели укрытий и т.д.
Для облегчения доступа к часто используемым графам удобно создать пользовательское расширение редактора:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| [CreateAssetMenu(fileName = "BehaviorLibrary", menuName = "Game/AI/Behavior Library")]
public class BehaviorLibrary : ScriptableObject
{
[Serializable]
public class BehaviorEntry
{
public string Name;
public string Description;
public BehaviorGraph Graph;
public Texture2D Icon;
}
public List<BehaviorEntry> Behaviors = new();
}
// Редактор для быстрого доступа к библиотеке
public class BehaviorLibraryWindow : EditorWindow
{
[MenuItem("Window/AI/Behavior Library")]
public static void ShowWindow()
{
GetWindow<BehaviorLibraryWindow>("Behavior Library");
}
// Реализация интерфейса окна...
} |
|
Благодаря этим практикам Behavior Graph становится не просто инструментом для создания AI, а платформой для построения масштабируемых поведенческих систем, где сложность и разнообразие поведения растут органически, без экспоненциального увеличения сложности разработки.
Продвинутые техники
Контекстно-зависимые переходы представляют собой мощный инструмент, позволяющий графам поведения принимать решения на основе текущего состояния игрового мира. В отличие от простых условных переходов, контекстно-зависимые переходы учитывают множество факторов и могут изменять свою логику в зависимости от исторического контекста выполнения. Реализация таких переходов осуществляется через комбинацию условных узлов и специальных оценочных функций. Например, можно создать узел, который оценивает текущую тактическую ситуацию и выбирает один из нескольких вариантов действий в зависимости от расположения противников, доступных укрытий и состояния самого агента:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public class TacticalEvaluatorNode : SequenceNode
{
[SerializeReference] public BlackboardVariable<List<GameObject>> Enemies;
[SerializeReference] public BlackboardVariable<List<GameObject>> CoverPoints;
[SerializeReference] public BlackboardVariable<float> Health;
[SerializeReference] public BlackboardVariable<TacticalAction> SelectedAction;
protected override Status OnUpdate()
{
float threatLevel = CalculateThreatLevel(Enemies.Value);
bool hasNearCover = FindClosestCover(CoverPoints.Value) != null;
// Принятие тактического решения на основе контекста
if (Health.Value < 0.3f && hasNearCover)
SelectedAction.Value = TacticalAction.TakeCover;
else if (threatLevel > 0.7f)
SelectedAction.Value = TacticalAction.Retreat;
else if (threatLevel < 0.3f)
SelectedAction.Value = TacticalAction.Attack;
else
SelectedAction.Value = TacticalAction.Flank;
return Status.Success;
}
// Методы оценки тактической ситуации...
} |
|
Многоуровневая архитектура AI с использованием вложенных графов позволяет создавать сложные, иерархические системы принятия решений. Верхние уровни отвечают за стратегические решения, средние — за тактические, а нижние — за конкретные действия. Такая структура не только упрощает разработку и отладку AI, но и делает его поведение более естественным и адаптивным. Типичная многоуровневая архитектура может включать:
1. Стратегический уровень: выбор глобальных целей (патрулирование, защита точки, атака базы игрока).
2. Тактический уровень: планирование конкретных действий для достижения выбранной стратегии (выбор пути, координация с союзниками).
3. Исполнительный уровень: реализация конкретных действий (перемещение, атака, использование способностей).
Каждый уровень может быть реализован как отдельный граф или субграф, что обеспечивает модульность и переиспользуемость компонентов. Взаимодействие между уровнями осуществляется через события и общие переменные блэкборда.
Процедурная генерация поведенческих схем открывает новые возможности для создания разнообразных и непредсказуемых противников. Вместо статических, предопределённых графов поведения, можно генерировать их динамически, адаптируя к ситуации или даже к стилю игры пользователя. Один из подходов — создание модульных фрагментов поведения, которые комбинируются по определённым правилам:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class BehaviorGenerator : MonoBehaviour
{
[SerializeField] private List<BehaviorGraph> _attackBehaviors;
[SerializeField] private List<BehaviorGraph> _defenseBehaviors;
[SerializeField] private List<BehaviorGraph> _specialAbilities;
private BehaviorGraph GenerateEnemyBehavior(EnemyType type, DifficultyLevel difficulty)
{
BehaviorGraph newGraph = ScriptableObject.CreateInstance<BehaviorGraph>();
// Добавление базовых узлов и переменных
AddCoreNodes(newGraph);
// Выбор подходящих для типа врага поведений
BehaviorGraph attackGraph = SelectAppropriate(_attackBehaviors, type);
BehaviorGraph defenseGraph = SelectAppropriate(_defenseBehaviors, type);
// Добавление случайных специальных способностей в зависимости от сложности
int abilitiesCount = Mathf.FloorToInt(difficulty) + Random.Range(0, 2);
for (int i = 0; i < abilitiesCount; i++)
{
AddSpecialAbility(newGraph, _specialAbilities[Random.Range(0, _specialAbilities.Count)]);
}
// Соединение всех компонентов в финальный граф
ConnectComponents(newGraph, attackGraph, defenseGraph);
return newGraph;
}
// Вспомогательные методы...
} |
|
Реализация памяти агента является ключевым аспектом для создания реалистичного AI. В Behavior Graph это можно реализовать несколькими способами:
1. Использование переменных блэкборда для хранения исторических данных (последние позиции игрока, результаты прошлых столкновений).
2. Создание специализированных структур данных для моделирования памяти (карта перемещений, список замеченных предметов).
3. Применение систем утилитарной оценки для принятия решений на основе накопленного опыта.
Пример реализации простой кратковременной памяти для преследования игрока:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| public class ShortTermMemoryNode : Action
{
[SerializeReference] public BlackboardVariable<GameObject> Player;
[SerializeReference] public BlackboardVariable<Vector3> LastKnownPosition;
[SerializeReference] public BlackboardVariable<float> MemoryDecayRate = new(0.1f);
[SerializeReference] public BlackboardVariable<float> Confidence = new(1.0f);
private float _lastUpdateTime;
protected override Status OnStart()
{
_lastUpdateTime = Time.time;
return Status.Running;
}
protected override Status OnUpdate()
{
// Проверка видимости игрока
if (CanSeePlayer())
{
// Обновление данных при прямом контакте
LastKnownPosition.Value = Player.Value.transform.position;
Confidence.Value = 1.0f;
_lastUpdateTime = Time.time;
}
else
{
// Постепенное снижение уверенности в старых данных
float timeSinceUpdate = Time.time - _lastUpdateTime;
Confidence.Value = Mathf.Max(0, Confidence.Value - MemoryDecayRate.Value * timeSinceUpdate);
}
return Status.Running;
}
private bool CanSeePlayer()
{
// Реализация проверки видимости...
return false;
}
} |
|
Интеграция Behavior Graph с другими системами Unity требует особого внимания к интерфейсам между компонентами. Наиболее распространённые сценарии интеграции включают:
1. Взаимодействие с навигационной системой Unity (NavMesh) для реализации перемещения.
2. Подключение к системам управления анимациями (Animator Controller, Animation Rigging).
3. Интеграция с физическим движком для реалистичного взаимодействия с окружением.
4. Связь с системами диалогов и сюжетных событий.
Для каждого типа интеграции рекомендуется создавать специализированные узлы-адаптеры, которые скрывают сложность взаимодействия систем и предоставляют простой интерфейс для использования в графах поведения.
Оптимизация производительности Behavior Graph становится критически важной при работе с большим количеством агентов. Основные стратегии оптимизации включают:
1. Кэширование результатов вычислений, особенно для дорогостоящих операций, таких как поиск пути или проверка видимости.
2. Использование уровней детализации для AI — упрощение поведения для удалённых или менее важных персонажей.
3. Распределение вычислений по кадрам, чтобы не все агенты обновляли своё состояние одновременно.
4. Пространственное разделение для оптимизации проверок восприятия и взаимодействия.
Для практической реализации распределения вычислений между кадрами можно использовать пул агентов с разнми приоритетами обновления:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class AIUpdateManager : MonoBehaviour
{
private List<BehaviorAgent> _allAgents = new();
private int _currentIndex = 0;
private int _agentsPerFrame = 5;
public void RegisterAgent(BehaviorAgent agent)
{
_allAgents.Add(agent);
}
private void Update()
{
// Обновляем только часть агентов каждый кадр
for (int i = 0; i < _agentsPerFrame; i++)
{
if (_currentIndex >= _allAgents.Count)
_currentIndex = 0;
_allAgents[_currentIndex].ManualUpdate();
_currentIndex++;
}
}
} |
|
Кроме оптимизации самих графов значительную роль играет то, как организованы данные, с которыми эти графы работают. Структуры пространственного разделения, такие как четвертичные деревья (quadtrees) или воксельные сетки, позволяют быстро находить ближайших противников или отбрасывать тех, кто находится вне зоны восприятия:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class SpatialPartitionNode : Action
{
[SerializeReference] public BlackboardVariable<GameObject> Self;
[SerializeReference] public BlackboardVariable<float> DetectionRadius;
[SerializeReference] public BlackboardVariable<List<GameObject>> VisibleEntities;
protected override Status OnUpdate()
{
VisibleEntities.Value.Clear();
// Запрос к структуре пространственного разделения вместо поиска по всем агентам
var nearbyEntities = GameWorld.SpatialGrid.QueryRadius(
Self.Value.transform.position,
DetectionRadius.Value
);
foreach (var entity in nearbyEntities)
{
if (IsVisible(Self.Value, entity))
VisibleEntities.Value.Add(entity);
}
return Status.Success;
}
} |
|
Нестандартные подходы к решению проблем с использованием Behavior Graph включают в себя применение концепций из смежных областей AI и их интеграцию в графовую структуру. Один из таких подходов — создание гибридных систем, объединяющих Behavior Graph с элементами нечёткой логики, генетических алгоритмов или машинного обучения. Например, узлы с нечёткой логикой могут принимать решения не на основе чётких условий "истина/ложь", а с учётом степени истинности:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| public class FuzzyEvaluatorNode : Action
{
[SerializeReference] public BlackboardVariable<float> Health;
[SerializeReference] public BlackboardVariable<float> Distance;
[SerializeReference] public BlackboardVariable<TacticalAction> SelectedAction;
protected override Status OnUpdate()
{
// Степени принадлежности к нечётким множествам
float lowHealth = FuzzyMembership(Health.Value, 0f, 0.4f);
float mediumHealth = FuzzyMembership(Health.Value, 0.3f, 0.7f);
float highHealth = FuzzyMembership(Health.Value, 0.6f, 1f);
float closeDistance = FuzzyMembership(Distance.Value, 0f, 5f);
float mediumDistance = FuzzyMembership(Distance.Value, 4f, 15f);
float farDistance = FuzzyMembership(Distance.Value, 12f, 30f);
// Нечёткие правила
float retreatScore = Math.Max(lowHealth * closeDistance, lowHealth * mediumDistance);
float attackScore = highHealth * closeDistance;
float flankerScore = mediumHealth * mediumDistance;
float sniperScore = Math.Max(mediumHealth * farDistance, highHealth * farDistance);
// Дефаззификация — выбор действия с максимальным значением
float maxScore = Math.Max(Math.Max(retreatScore, attackScore),
Math.Max(flankerScore, sniperScore));
if (maxScore == retreatScore)
SelectedAction.Value = TacticalAction.Retreat;
else if (maxScore == attackScore)
SelectedAction.Value = TacticalAction.Attack;
else if (maxScore == flankerScore)
SelectedAction.Value = TacticalAction.Flank;
else
SelectedAction.Value = TacticalAction.Snipe;
return Status.Success;
}
private float FuzzyMembership(float value, float min, float max)
{
if (value <= min) return 0f;
if (value >= max) return 1f;
return (value - min) / (max - min);
}
} |
|
Другой нестандартный подход — использование метаграфов, то есть графов, которые модифицируют другие графы. Это позволяет создавать системы, которые эволюционируют и адаптируются во время игры:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public class GraphEvolutionSystem : MonoBehaviour
{
[SerializeField] private BehaviorGraph _baseGraph;
[SerializeField] private List<BehaviorGraph> _strategyTemplates;
[SerializeField] private List<BehaviorAgent> _agents;
private Dictionary<BehaviorAgent, PerformanceMetrics> _agentPerformance = new();
public void EvolveAgentStrategies()
{
// Сортировка агентов по эффективности
var rankedAgents = _agentPerformance.OrderByDescending(pair => pair.Value.OverallScore).ToList();
// Замена стратегий у наименее эффективных агентов
for (int i = rankedAgents.Count / 2; i < rankedAgents.Count; i++)
{
var agent = rankedAgents[i].Key;
// Мутация графа одного из лучших агентов
var bestAgentGraph = rankedAgents[Random.Range(0, rankedAgents.Count / 4)].Key.Graph;
var newGraph = MutateGraph(bestAgentGraph);
// Применение нового графа
agent.SetGraph(newGraph);
}
}
private BehaviorGraph MutateGraph(BehaviorGraph sourceGraph)
{
// Реализация мутации графа...
// Может включать изменение параметров, замену узлов или перестройку связей
return null; // Вернуть мутированный граф
}
} |
|
Интеграция Behavior Graph с системами процедурной генерации контента открывает возможности для создания игр с динамически меняющимися правилами и поведением NPC. Например, система может анализировать стиль игры пользователя и создавать персонажей, которые лучше противостоят его стратегиям:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public class AdaptiveOpponentGenerator : MonoBehaviour
{
[SerializeField] private PlayerDataAnalyzer _playerAnalyzer;
[SerializeField] private BehaviorTemplateLibrary _templates;
public BehaviorGraph GenerateCounterBehavior()
{
PlayerStyle playerStyle = _playerAnalyzer.GetDominantStyle();
// Выбор шаблонов, эффективных против данного стиля
List<BehaviorTemplate> counterTemplates = _templates.GetCounters(playerStyle);
// Комбинирование шаблонов с небольшими вариациями
BehaviorGraph resultGraph = CombineTemplates(counterTemplates,
playerStyle.Aggression,
playerStyle.Patience);
return resultGraph;
}
private BehaviorGraph CombineTemplates(List<BehaviorTemplate> templates,
float aggression,
float patience)
{
// Логика комбинирования шаблонов с учётом параметров
return null;
}
} |
|
Одним из наиболее мощных нестандартных подходов является совмещение Behavior Graph с нейронными сетями. Узлы графа могут использовать предварительно обученные нейросети для принятия решений, распознавания образов или предсказания действий игрока:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class NeuralNetworkNode : Action
{
[SerializeReference] public BlackboardVariable<float[]> InputFeatures;
[SerializeReference] public BlackboardVariable<float[]> OutputValues;
private NeuralNetwork _network;
protected override Status OnStart()
{
// Загрузка предварительно обученной модели
_network = NeuralNetworkLoader.Load("combat_tactics_model");
return Status.Running;
}
protected override Status OnUpdate()
{
// Передача входных данных в нейросеть и получение результатов
OutputValues.Value = _network.Predict(InputFeatures.Value);
return Status.Success;
}
} |
|
Такая гибридная система позволяет объединить лучшие качества обоих подходов: структурированность и понятность графов поведения с гибкостью и адаптивностью машинного обучения. Важно отметить, что нестандартные подходы часто требуют больше вычислительных ресурсов и тщательной балансировки. В продакшн-проектах они нередко применяются выборочно — например, только для ключевых персонажей или особых ситуаций, где стандартные решения не дают достаточно убедительных результатов.
Разбор конкретных сценариев
Теоретическое понимание Behavior Graph — лишь половина дела. Практическое применение инструмента в конкретных игровых сценариях демонстрирует его настоящую мощь. Разберем несколько практических кейсов, которые можно реализовать с помощью этой технологии.
Адаптивное поведение NPC в открытом мире
Открытый мир предъявляет особые требования к AI персонажей. NPC должны адаптироваться к динамически меняющимся условиям и действиям игрока. С помощью Behavior Graph можно создать систему, которая масштабируется от базовых действий до сложных реакций. Рассмотрим пример деревенского жителя в RPG. Днем он занимается своими делами — работает в поле, ходит на рынок, общается с соседями. Вечером возвращается домой, ужинает и ложится спать. При нападении врагов он может убегать или, если обучен, браться за оружие.
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // Узел, определяющий распорядок дня NPC
public class DailyRoutineNode : Action
{
[SerializeReference] public BlackboardVariable<GameObject> Self;
[SerializeReference] public BlackboardVariable<DayTime> CurrentTime;
[SerializeReference] public BlackboardVariable<RoutineActivity> SelectedActivity;
[SerializeReference] public BlackboardVariable<GameObject> ActivityLocation;
protected override Status OnUpdate()
{
// Определение активности на основе времени суток
switch (CurrentTime.Value)
{
case DayTime.Morning:
SelectedActivity.Value = RoutineActivity.Field;
ActivityLocation.Value = GameLocations.Field;
break;
case DayTime.Noon:
SelectedActivity.Value = RoutineActivity.Market;
ActivityLocation.Value = GameLocations.Market;
break;
case DayTime.Evening:
SelectedActivity.Value = RoutineActivity.Home;
ActivityLocation.Value = GameLocations.House;
break;
case DayTime.Night:
SelectedActivity.Value = RoutineActivity.Sleep;
ActivityLocation.Value = GameLocations.Bed;
break;
}
return Status.Success;
}
} |
|
Для врагов разного типа можно создать базовый шаблон поведения, который затем модифицируется в зависимости от класса противника. Например, для фэнтезийной игры:
1. Громилы: приоритет прямой атаки, игнорирование повреждений до определенного порога.
2. Лучники: поддержание дистанции, поиск высоких позиций.
3. Маги: чередование заклинаний, телепортация при опасности.
Основа этих поведений может быть реализована через один граф с условными ветвлениями на основе типа врага:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // Часть графа для выбора атакующего поведения
public class SelectAttackPatternNode : Action
{
[SerializeReference] public BlackboardVariable<EnemyType> Type;
[SerializeReference] public BlackboardVariable<GameObject> Player;
[SerializeReference] public BlackboardVariable<AttackPattern> SelectedPattern;
[SerializeReference] public BlackboardVariable<float> DistanceToPlayer;
protected override Status OnUpdate()
{
DistanceToPlayer.Value = Vector3.Distance(
Self.Value.transform.position,
Player.Value.transform.position
);
switch (Type.Value)
{
case EnemyType.Brute:
SelectedPattern.Value = AttackPattern.DirectAssault;
break;
case EnemyType.Archer:
if (DistanceToPlayer.Value < 5.0f)
SelectedPattern.Value = AttackPattern.CreateDistance;
else if (DistanceToPlayer.Value > 15.0f)
SelectedPattern.Value = AttackPattern.ApproachSlightly;
else
SelectedPattern.Value = AttackPattern.RangedAttack;
break;
case EnemyType.Mage:
if (Health.Value < 0.3f)
SelectedPattern.Value = AttackPattern.Teleport;
else if (ManaPool.Value > 0.5f)
SelectedPattern.Value = AttackPattern.CastPowerful;
else
SelectedPattern.Value = AttackPattern.CastWeak;
break;
}
return Status.Success;
}
} |
|
Сложные поведенческие паттерны, такие как заманивание игрока в засаду или координированная атака, реализуются через многоуровневые графы, где высший уровень определяет стратегию, а нижние — конкретные тактики.
Командная координация AI с помощью связанных графов
Особую сложность представляет координация действий между несколькими AI агентами. Behavior Graph предлагает элегантное решение через систему событий и общие переменные. Рассмотрим отряд из трех противников с разными ролями: лидер, поддержка и штурмовик.
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Канал событий для команды
[CreateAssetMenu(fileName = "SquadEvents", menuName = "Game/AI/Squad Events")]
public class SquadEventChannel : EventChannel
{
public enum CommandType { Attack, Flank, Support, Retreat }
public CommandType Command { get; private set; }
public Vector3 TargetPosition { get; private set; }
public void IssueCommand(CommandType cmd, Vector3 position)
{
Command = cmd;
TargetPosition = position;
Invoke();
}
} |
|
Лидер анализирует ситуацию и принимает решения, которые транслируются остальным членам отряда через события:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // В графе лидера отряда
protected override Status OnUpdate()
{
// Анализ тактической ситуации
if (TeamHealth.Value < 0.3f)
{
SquadEvents.Value.IssueCommand(CommandType.Retreat, RetreatPoint.Value);
}
else if (CanFlankPlayer.Value)
{
SquadEvents.Value.IssueCommand(CommandType.Flank, FlankPosition.Value);
}
else
{
SquadEvents.Value.IssueCommand(CommandType.Attack, Player.Value.transform.position);
}
return Status.Success;
} |
|
Члены отряда слушают эти события и корректируют своё поведение:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // В графе члена отряда: узел, который подписывается на команды
public class SquadMemberNode : Action
{
[SerializeReference] public BlackboardVariable<SquadEventChannel> SquadEvents;
[SerializeReference] public BlackboardVariable<CommandType> CurrentCommand;
[SerializeReference] public BlackboardVariable<Vector3> TargetPosition;
private EventHandler _commandHandler;
protected override Status OnStart()
{
// Подписка на события
_commandHandler = (sender, args) => {
CurrentCommand.Value = SquadEvents.Value.Command;
TargetPosition.Value = SquadEvents.Value.TargetPosition;
};
SquadEvents.Value.OnEventRaised += _commandHandler;
return Status.Running;
}
protected override void OnStop()
{
// Отписка при завершении
SquadEvents.Value.OnEventRaised -= _commandHandler;
}
protected override Status OnUpdate()
{
// Постоянно работающий узел, который слушает команды
return Status.Running;
}
} |
|
Эмоциональные модели поведения на основе Behavior Graph
Добавление эмоциональной составляющей делает NPC более живми и реалистичными. Эмоции влияют на принятие решений, скорость передвижения, выбор реплик и даже анимации. Для моделирования эмоций можно использовать несколько параметров:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| public class EmotionalStateNode : Action
{
[SerializeReference] public BlackboardVariable<float> Fear = new(0f);
[SerializeReference] public BlackboardVariable<float> Anger = new(0f);
[SerializeReference] public BlackboardVariable<float> Joy = new(0f);
[SerializeReference] public BlackboardVariable<float> EmotionalDecayRate = new(0.01f);
[SerializeReference] public BlackboardVariable<EmotionalState> DominantEmotion;
protected override Status OnUpdate()
{
// Естественное затухание эмоций со временем
Fear.Value = Mathf.Max(0, Fear.Value - EmotionalDecayRate.Value * Time.deltaTime);
Anger.Value = Mathf.Max(0, Anger.Value - EmotionalDecayRate.Value * Time.deltaTime);
Joy.Value = Mathf.Max(0, Joy.Value - EmotionalDecayRate.Value * Time.deltaTime);
// Определение доминирующей эмоции
if (Fear.Value > Anger.Value && Fear.Value > Joy.Value && Fear.Value > 0.3f)
DominantEmotion.Value = EmotionalState.Fear;
else if (Anger.Value > Fear.Value && Anger.Value > Joy.Value && Anger.Value > 0.3f)
DominantEmotion.Value = EmotionalState.Anger;
else if (Joy.Value > 0.3f)
DominantEmotion.Value = EmotionalState.Joy;
else
DominantEmotion.Value = EmotionalState.Neutral;
return Status.Running;
}
} |
|
Эмоциональное состояние затем влияет на другие аспекты поведения:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Модификация скорости перемещения на основе эмоций
protected override Status OnUpdate()
{
float baseSpeed = MovementSpeed.Value;
switch (EmotionalState.Value)
{
case EmotionalState.Fear:
CurrentSpeed.Value = baseSpeed * 1.5f; // Бег от страха
break;
case EmotionalState.Anger:
CurrentSpeed.Value = baseSpeed * 1.2f; // Агрессивное движение
break;
case EmotionalState.Joy:
CurrentSpeed.Value = baseSpeed * 0.8f; // Расслабленное движение
break;
default:
CurrentSpeed.Value = baseSpeed;
break;
}
return Status.Success;
} |
|
Масштабирование AI систем для массовых сцен
Городские площади, поля сражений, толпы демонстрантов — все эти сценарии требуют одновременной работы десятков или сотен AI-агентов. Чтобы избежать падения производительности, необходимо применять специальные техники оптимизации.
Одна из стратегий — использование разных уровней детализации AI (AI LOD):
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public class AILODManager : MonoBehaviour
{
[SerializeField] private Transform _player;
[SerializeField] private float _highDetailDistance = 10f;
[SerializeField] private float _mediumDetailDistance = 30f;
private List<BehaviorAgent> _allAgents = new();
public void RegisterAgent(BehaviorAgent agent)
{
_allAgents.Add(agent);
}
private void Update()
{
foreach (var agent in _allAgents)
{
float distance = Vector3.Distance(_player.position, agent.transform.position);
if (distance <= _highDetailDistance)
{
agent.SetLODLevel(AI_LOD.High); // Полный граф поведения
}
else if (distance <= _mediumDetailDistance)
{
agent.SetLODLevel(AI_LOD.Medium); // Упрощенный граф
}
else
{
agent.SetLODLevel(AI_LOD.Low); // Минимальные обновления
}
}
}
} |
|
Для каждого уровня детализации можно иметь отдельный субграф поведения:
Высокий LOD: полная эмоциональная модель, сложные тактики, проверка дальних точек видимости.
Средний LOD: базовые реакции, упрощенная навигация.
Низкий LOD: только анимация "присутствия", без сложной логики.
Важно отметить, что некоторые ключевые NPC (главные враги, важные персонажи) могут всегда использовать высокий уровень детализации независимо от расстояния до игрока.
Другой интересный сценарий — групповое поведение когда большое количество юнитов действует как единое целое. Это актуально для стай животных, отрядов солдат или толп горожан. Для его реализации эффективно использовать комбинацию централизованного управления и индивидуального поведения:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class FlockManager : MonoBehaviour
{
[SerializeField] private List<BehaviorAgent> _flockMembers = new();
[SerializeField] private Vector3 _flockCenter;
[SerializeField] private Vector3 _flockDirection;
[SerializeField] private float _cohesionStrength = 1.0f;
[SerializeField] private float _alignmentStrength = 0.8f;
[SerializeField] private float _separationStrength = 1.2f;
void Update()
{
CalculateFlockProperties();
BroadcastToMembers();
}
private void CalculateFlockProperties()
{
// Расчёт общих свойств стаи
Vector3 centerSum = Vector3.zero;
Vector3 directionSum = Vector3.zero;
foreach (var member in _flockMembers)
{
centerSum += member.transform.position;
// Проверяем, что Rigidbody существует
var rb = member.GetComponent<Rigidbody>();
if (rb != null)
directionSum += rb.velocity.normalized;
}
_flockCenter = centerSum / _flockMembers.Count;
_flockDirection = directionSum.normalized;
}
private void BroadcastToMembers()
{
foreach (var member in _flockMembers)
{
member.SetBlackboardValue("FlockCenter", _flockCenter);
member.SetBlackboardValue("FlockDirection", _flockDirection);
member.SetBlackboardValue("CohesionStrength", _cohesionStrength);
member.SetBlackboardValue("AlignmentStrength", _alignmentStrength);
member.SetBlackboardValue("SeparationStrength", _separationStrength);
}
}
} |
|
В графе поведения каждого члена стаи используются эти общие параметры для корректировки собственного движения:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public class FlockMemberBehaviorNode : Action
{
[SerializeReference] public BlackboardVariable<Vector3> FlockCenter;
[SerializeReference] public BlackboardVariable<Vector3> FlockDirection;
[SerializeReference] public BlackboardVariable<float> CohesionStrength;
[SerializeReference] public BlackboardVariable<float> AlignmentStrength;
[SerializeReference] public BlackboardVariable<float> SeparationStrength;
protected override Status OnUpdate()
{
// Получение компонентов
var rb = Self.Value.GetComponent<Rigidbody>();
if (rb == null)
return Status.Failure;
// Расчёт векторов влияния
Vector3 cohesion = (FlockCenter.Value - Self.Value.transform.position).normalized;
Vector3 alignment = FlockDirection.Value;
Vector3 separation = CalculateSeparation();
// Комбинирование сил
Vector3 combinedForce = cohesion * CohesionStrength.Value +
alignment * AlignmentStrength.Value +
separation * SeparationStrength.Value;
// Применение результирующей силы с ограничением скорости
rb.velocity = Vector3.ClampMagnitude(rb.velocity + combinedForce, MaxSpeed.Value);
return Status.Running;
}
private Vector3 CalculateSeparation()
{
// Расчёт вектора разделения, отталкивающего от соседей
// ...
return Vector3.zero; // Упрощённый вариант
}
} |
|
Интеграция Behavior Graph с системами машинного обучения
Машинное обучение открывает новые горизонты для игрового AI, а Behavior Graph может служить эффективным мостом между традиционным программированием и обученными моделями. Рассмотрим несколько подходов к такой интеграции.
Первый подход — использование предобученных моделей для принятия тактических решений. Модель обучается на большом наборе данных (например, записях игровых сессий профессиональных игроков) и затем интегрируется в граф поведения:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| public class ML_TacticalDecisionNode : Action
{
[SerializeReference] public BlackboardVariable<float[]> ContextFeatures;
[SerializeReference] public BlackboardVariable<int> SelectedTactic;
private TacticPredictor _predictor;
protected override Status OnStart()
{
// Загрузка модели при первом запуске
_predictor = new TacticPredictor("Assets/ML/TacticModel.onnx");
return Status.Running;
}
protected override Status OnUpdate()
{
// Подготовка входных данных для модели
var input = PreprocessInput(ContextFeatures.Value);
// Получение предсказания
var output = _predictor.Predict(input);
// Выбор тактики с наивысшей вероятностью
SelectedTactic.Value = GetMaxProbabilityIndex(output);
return Status.Success;
}
private float[] PreprocessInput(float[] rawFeatures)
{
// Нормализация, масштабирование и другая предобработка
return rawFeatures;
}
private int GetMaxProbabilityIndex(float[] probabilities)
{
int maxIndex = 0;
float maxValue = probabilities[0];
for (int i = 1; i < probabilities.Length; i++)
{
if (probabilities[i] > maxValue)
{
maxValue = probabilities[i];
maxIndex = i;
}
}
return maxIndex;
}
} |
|
Второй подход — обучение с подкреплением непосредственно в игре. AI-агенты взаимодействуют с игровым миром, получают награды за желаемое поведение и постепенно улучшают свою стратегию:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| public class RL_AgentTrainer : MonoBehaviour
{
[SerializeField] private BehaviorAgent _agent;
[SerializeField] private float _learningRate = 0.01f;
[SerializeField] private float _discountFactor = 0.95f;
private Dictionary<string, float> _qTable = new();
private string _lastState;
private string _lastAction;
public void RecordState(string state, string action, float reward)
{
string stateAction = $"{state}:{action}";
// Обновление Q-значения для состояния и действия
if (!_qTable.ContainsKey(stateAction))
_qTable[stateAction] = 0;
if (_lastState != null)
{
string lastStateAction = $"{_lastState}:{_lastAction}";
// Q-learning формула
float maxQ = GetMaxQForState(state);
_qTable[lastStateAction] += _learningRate * (reward + _discountFactor * maxQ - _qTable[lastStateAction]);
}
_lastState = state;
_lastAction = action;
// Сохранение модели периодически
if (Time.frameCount % 1000 == 0)
SaveModel();
}
public string GetBestAction(string state, List<string> possibleActions)
{
// Исследование vs использование (exploration vs exploitation)
if (Random.value < GetExplorationRate())
return possibleActions[Random.Range(0, possibleActions.Count)];
// Выбор действия с максимальным Q-значением
float maxQ = float.MinValue;
string bestAction = possibleActions[0];
foreach (var action in possibleActions)
{
string stateAction = $"{state}:{action}";
float q = _qTable.ContainsKey(stateAction) ? _qTable[stateAction] : 0;
if (q > maxQ)
{
maxQ = q;
bestAction = action;
}
}
return bestAction;
}
private float GetMaxQForState(string state)
{
// Поиск максимального Q-значения для состояния
float maxQ = float.MinValue;
foreach (var entry in _qTable)
{
if (entry.Key.StartsWith(state + ":") && entry.Value > maxQ)
maxQ = entry.Value;
}
return maxQ == float.MinValue ? 0 : maxQ;
}
private float GetExplorationRate()
{
// Снижение вероятности случайных действий со временем
return Mathf.Max(0.1f, 1.0f - GameManager.Instance.GameTime / 3600f);
}
private void SaveModel()
{
// Сохранение Q-таблицы
}
} |
|
В Behavior Graph этот тренер интегрируется через специальный узел:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class RL_ActionSelectorNode : Action
{
[SerializeReference] public BlackboardVariable<RL_AgentTrainer> Trainer;
[SerializeReference] public BlackboardVariable<string> CurrentState;
[SerializeReference] public BlackboardVariable<List<string>> PossibleActions;
[SerializeReference] public BlackboardVariable<string> SelectedAction;
protected override Status OnUpdate()
{
if (Trainer.Value == null)
return Status.Failure;
SelectedAction.Value = Trainer.Value.GetBestAction(
CurrentState.Value,
PossibleActions.Value
);
return Status.Success;
}
} |
|
Третий подход — гибридные системы, где Behavior Graph определяет высокоуровневую структуру поведения, а специфические решения делегирует нейронным сетям:
C#
Скопировано | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| public class HybridAI_CombatNode : Action
{
[SerializeReference] public BlackboardVariable<GameObject> Enemy;
[SerializeReference] public BlackboardVariable<CombatContext> Context;
[SerializeReference] public BlackboardVariable<CombatStyle> Style;
private PosePredictor _posePredictor;
private AttackEvaluator _attackEvaluator;
protected override Status OnStart()
{
// Загрузка специализированных моделей
_posePredictor = new PosePredictor("Assets/ML/PoseModel.onnx");
_attackEvaluator = new AttackEvaluator("Assets/ML/AttackModel.onnx");
return Status.Running;
}
protected override Status OnUpdate()
{
if (Enemy.Value == null)
return Status.Failure;
// Традиционная логика для высокоуровневых решений
if (Context.Value.Health < 0.2f)
{
Style.Value = CombatStyle.Defensive;
}
else if (Context.Value.Stamina > 0.8f)
{
Style.Value = CombatStyle.Aggressive;
}
// ML для предсказания следующего действия противника
var enemyPose = GetEnemyPoseFeatures();
var nextPosePrediction = _posePredictor.Predict(enemyPose);
// ML для оценки эффективности разных атак
var attackContext = GetAttackContext(nextPosePrediction);
var attackScores = _attackEvaluator.Evaluate(attackContext);
// Выбор лучшей атаки на основе оценок ML и текущего стиля
SelectBestAttack(attackScores, Style.Value);
return Status.Running;
}
// Вспомогательные методы
} |
|
Такая гибридная архитектура позволяет сочетать предсказуемость и объяснимость традиционных алгоритмов с адаптивностью и сложностью моделей машинного обучения. Для эффективной интеграции ML с Behavior Graph рекомендуется:
1. Использовать легковесные модели, оптимизированные для мобильных платформ.
2. Кэшировать результаты предсказаний, чтобы не запускать модель на каждом кадре.
3. Применять модели только для ключевых аспектов игрового процесса.
4. Подготовить запасные алгоритмы на случай проблем с загрузкой или выполнением модели.
Важно понимать, что машинное обучение — не панацея, и его целесообразно применять там, где традиционные алгоритмы достигают своего предела. Простые задачи, такие как навигация или базовое принятие решений, часто эффективнее решаются классическими методами.
Итоги и перспективы
Сравнивая производительность Behavior Graph с другими решениями для AI, стоит отметить сбалансированный подход к использованию ресурсов. В отличие от тяжеловесных систем планирования, ориентированных на цели (GOAP), которые могут создавать существенную нагрузку при перерасчёте планов, Behavior Graph распределяет вычисления между кадрами. Это делает его более предсказуемым с точки зрения потребления CPU. При сравнении с конечными автоматами (FSM), Behavior Graph демонстрирует превосходство в гибкости и масштабируемости, хотя при малом количестве состояний FSM может быть производительнее. Utility AI-системы часто требуют вычисления множества утилитарных функций для каждого возможного действия, что может стать узким местом при большом количестве вариантов, в то время как графовый подход позволяет эффективно отсекать неактуальные ветви.
Несмотря на все преимущества, Behavior Graph имеет определённые ограничения. Текущая версия (1.0.3) всё ещё содержит некоторые баги, что делает её применение в производственных проектах рискованным. Разработчики должны тщательно тестировать графы на предмет неожиданного поведения, особенно в сложных сценариях с множеством событий и параллельно выполняющихся ветвей.
Другое ограничение связано с соблазном использовать Behavior Graph как замену визуальному скриптингу, что может привести к созданию запутанных графов с низким уровнем абстракции. Важно помнить, что этот инструмент предназначен для моделирования AI-поведения, а не для реализации общей игровой логики.
Что касается перспектив развития, Behavior Graph имеет значительный потенциал. С развитием технологии можно ожидать:- Расширения интеграции с системами машинного обучения, что позволит создавать гибридные AI-системы с адаптивным поведением.
- Улучшения инструментов для анализа и оптимизации графов, включая профилирование узких мест и рекомендации по реструктуризации.
- Появления специализированных шаблонов и библиотек для различных игровых жанров, что ускорит разработку типовых AI-систем.
Behavior Graph заполняет важный пробел в экосистеме Unity, предоставляя разработчикам нативный инструмент для реализации сложной AI-логики без необходимости прибегать к сторонним решениям. По мере взросления технологии и расширения сообщества пользователей, мы, вероятно, увидим появление передовых практик и паттернов, которые ещё больше упростят создание убедительного искусственного интеллекта в играх. В будущих обновлениях ожидается как расширение базового функционала, так и улучшение производительности, что позволит применять Behavior Graph даже в самых требовательных проектах с сотнями или тысячами AI-агентов, действующих одновременно.
Behavior для привязки свойств только для чтения [WPF, Элд Хасп] Начал осваивать как создавать Behavior.
Первый создал такой для привязки свойств только для чтения... TreeView Behavior Click Всем доброго дня.
Сразу напишу данную реализацию нашел здесь на форуме. Реализация skilllab.
Она... Behavior не оповещает об изменении Я нажимаю на элемент и хочу прибиндить его результат к ViewModel. Допустим у меня есть какой-то... Разница между AttachedProperty и Behavior<> Не могу понять разницу между Behavior и AttachedProperty.
Недавно я реализовывал ThemeChanger... Behavior и кастомное свойство Command Я написал простой Behavior, чтобы перемещать элемент в Canvas'е.
В ViewModel создал команду и... Утечка памяти в behavior TabControl Есть замечательный behavior для TabControl... "Кривая" UV Разметка в Unity Shader Graph Здраствуйте, начал изучение Unity Shader Graph, но столкнулся с проблемой:
Шейдер накладывается... Unity Shader Graph - не работает Tiling And Offset Я хотел сделать тайловый клетчатый фон при помощи шейдера, но столкнулся с проблемой, что нод... Unity Shader Graph - отображение превью нода Подскажите, как настроить тип отображения превью нода - чтобы получаемая текстура отображалась не... Что лучше учить: Shader Graph или ЯП шейдеров unity? Здравствуйте, решил начать изучать шейдеры. И сразу я встал перед выбором: учить мне новомодный... Ошибка при создании дескриптора окна при динамическом создании кнопок public Form1()
{
InitializeComponent();
}
int height = 10;... Как использовать компонент Microsoft Graph 2000 в ASP? Привет всем. Кто может подсказат как исползоват Microsoft Graph 2000 .
Или подсказат другои...
|