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 { |
И мы можем настраивать параметры через кривые в инспекторе:
Финальная оценка тоже может быть настроена как математическая операция для данных со всех кривых: