UtilityAI - реализация для LeoECS Proto
UtilityAI
- еще один популярный подход наравне с GOAP
для реализации поведения ИИ на основе настраиваемых правил. В чем же разница?
Введение
GOAP
представляет собой набор действий, выполняя которые бот должен достичь определенной цели из его текущего состояния. Если нет последовательности действий, по которым можно “пройти” до цели - данный подход не предлагает ничего в качестве запасного поведения по умолчанию. Т.е мы или можем достичь цели одним самым оптимальным путем, либо у нас нет решения в принципе - мы не можем пройти половину пути, а потом на месте решить что делать дальше. Можно так же почитать статью AI GOAP - планировщик целеориентированного ИИ
UtilityAI
представляет собой набор таких же действий, но имеющих собственную оценку целесообразности на основе текущих параметров бота. Эти действия просто линейно оцениваются и выбирается самое ценное действие на текущий момент. Т.е мы всегда будем иметь какое-то поведение бота в любой момент времени.
Это основное отличие этих двух решений - GOAP
выполняет планирование действий для достижения цели, UtilityAI
просто выбирает действие прямо сейчас без любого дальнейшего планирования.
Основные определения UtilityAI
- Решение. Какое-то абстрактное поведение, имеющее “ценность”.
UtilityAI
ничего не знает о том, что происходит вРешении
и может только запрашивать у него оценку целесообразности. - Параметры. Какие-то внешние данные, на основе которых происходит оценка целесообразности
Решения
. Параметров может не быть (например, для какого-то действия по умолчанию типа “Ожидания” нет в них нужды), может быть 1, 2, 3 или больше. В случае 2-хПараметров
и более, оценка все-равно должна быть представлена одним значением, по которому будет производиться сравнительная оценка междуРешениями
для выбора лучшего. - Оценщик. Сердце
UtilityAI
и одновременно - ее самая простая часть: принимает запрос на вычисление следующего лучшегоРешения
, вызывает оценку всех действий с передачейПараметров
, выбирает вариант с максимальным значением и возвращает его как результат своей работы.
Пример
Рассмотрим простой пример для LeoECS Proto
- бот знает о своем “Голоде”, знает количество “Еды” в карманах, умеет “Ждать”, “Искать еду” и “Есть еду”.
“Голод” и “Еда” - это Параметры
.
“Ждать”, “Искать еду” и “Есть еду” - это Решения
.
Давайте переведем это в код.
Параметры
Параметры
будут храниться в ECS
-компоненте как его поля:
1 | // Компонент |
Для использования в мире нам потребуется аспект с описанным компонентом:
1 | // Аспект с пулами. |
Решения
Решения
должны реализовывать специальный интерфейс (в рамках реализации UtilityAI для LeoECS Proto
).
Решение
“Поиск еды”:
1 | class SearchFoodSolver : IAiUtilitySolver { |
Решение
“Употребление еды”:
1 | class EatFoodSolver : IAiUtilitySolver { |
Решение
“Ожидание”:
1 | class WaitSolver : IAiUtilitySolver { |
ECS-системы настройки, изменения и применения изменений
Система создания и инициализации бота:
1 | class UnitInitSystem : IProtoInitSystem { |
Система начисления “голода” всем ботам:
1 | class HungerSystem : IProtoRunSystem { |
Система получения и применения найденного Решения
:
1 | class CheckSystem : IProtoRunSystem { |
Модуль UtilityAI для LeoECS Proto
работает по схеме “Запрос-Ответ” - мы вешаем специальный компонент-запрос на сущность с данными бота и ожидаем компонента-ответа на той же сущности. За компонентами запроса-ответа специально следить не нужно - они добавляются и удаляются модулем UtilityAI
автоматически.
Давайте свяжем все эти ECS
-системы и Решения
кодом запуска ECS Proto
в виде теста (ECS
позволяет относительно легко писать unit-тесты):
1 | [ ] |
Тест успешно пройден, были приняты и выполнены решения:
Голод=50
,Еда=0
: самое ценное решениеWaitSolver
имеет важность 0.5 - “ничего не делаем” ->Голод=50
,Еда=0
.Голод=100
,Еда=0
: самое ценное решениеSearchFoodSolver
имеет важность 1.0 - “ищем и находим еду” ->Голод=100
,Еда=1
.Голод=150
,Еда=1
: самое ценное решениеEatFoodSolver
имеет важность 1.0 - “едим” ->Голод=0
,Еда=0
.Голод=50
,Еда=0
: самое ценное решениеWaitSolver
имеет важность 0.5 - “ничего не делаем” ->Голод=50
,Еда=0
.
Специально для юнити был добавлен пакет, позволяющий редактировать данные в виде кривых в ScriptableObject
и использовать их в Решениях
:
Можно использовать готовые типы на 1-2-3 параметра, можно реализовать свои, а затем любым способом передать в Решение
(инъекция через конструктор или [DI]
). “Поиск еды” тогда преобразуется в следующий вид:
1 | class SearchFoodSolver : IAiUtilitySolver { |
И мы можем настраивать параметры через кривые в инспекторе:
Финальная оценка тоже может быть настроена как математическая операция для данных со всех кривых: