Rule-based AI + Unity
Привет! В прошлой статье мы уже говорили про искусственный интеллект и трудности его выбора. В этой мы поговорим как начать его писать так чтобы потом не выстрелить себе в ногу.
Возвращаясь к пресловутой шкале “сложности искусственного интеллекта”, по классике все что находится левее State Machine, называют каким-нибудь Scripting AI/Non-scripted AI/No-framework AI и так далее. Так же как и названия, сами они представляют из себя хаотичный набор условий и поведений, реализованный без какого-либо централизованного подхода. Там нет выделенных состояний, переходов и любых других переиспользуемых компонентов-кирпичиков, из которых состоят все остальные подходы.
В качестве примера - птица в open-world игре. Она летит через всю карту по заданному маршруту, при попадании в нее падает, а через 5 секунд после падения исчезает с каким-нибудь эффектом. Для такого поведения городить фреймворки будет себе дороже.
Но есть один подход, который находится между вышеуказанным “подходом” и остальными классическими подходами для создания игрового ИИ. В книжке Game AI Pro он именуется Rule-Based AI (не путать Rule Based AI, применяемый на совершенно другом уровне ИИ).
Его плюсы:
- Гораздо проще более “серьезных” подходов типа FMS, BT, Utility AI и GOAP
- Легко трансформируется в любой из этих подходов при необходимости (хотя не понимаю кому в здравом уме придет мысль “о, теперь мы все обдумали и решили, что будем писать ИИ на стейт машине”)
- Очень гибкий и достаточно долго может существовать как самостоятельное решение, если игра не требует огромного количества поведений или продвинутых алгоритмов их смены
В этой части статьи разберемся что это и как реализовать в Unity, а в следующих - как от него при необходимости переходить к более серьезным подходам. Вместе с причинами, по которым это стоит или не стоит делать.
Чтобы увидеть больше контента по разработке игр и игровому ИИ в частности, добро пожаловать на мой канал:
Определение
Rule-based AI состоит из коллекции пар предикат-действие. Мы проверяем все предикаты и для первого, у которого этот предикат истинный, выполняем действие.
Поехали!
1. Создаем “фреймворк”
В качестве примера у нас будут игроки, которые найдут ближайшего противника, подойдут к нему, и наносить урон пока противник еще существует. И дальше по новой.
Сначала определим наши основные классы для ИИ:
И сам актор, с которым будет взаимодействовать игры:
Проще некуда, но за этой простотой скрывается большой потенциал.
2. Создаем точку входа и игровые сервисы
Создадим пока что набросок для точки входа. В ней мы определим, как будут создаваться наши персонажи и как их поведение будет обрабатываться.
Теперь посмотрим, что нам нужно для реализации этой логики. Метод “CreateActor” подсказывает нам о создании фабрики для персонажей, а метод “UpdateActors” - репозитория с ними.
Начнем наоборот с репозитория. Внутри нам нужны только методы добавления, удаления сущностей, выполнения какого-нибудь метода для каждого из них и сама коллекция для выборки. Сделаем также класс обобщенным, чтобы хранить Actor’ов и персонажей отдельно:
Обратите внимание, что при переборе объектов в ForEach мы делаем дополнительный массив методом ToArray(). Иначе при большом количестве персонажей мы можем поймать ошибку, что в момент перебора массива извне из него будут добавляться или удаляться элементы.
Теперь, когда нам есть куда складывать персонажей, сделаем фабрику для их создания:
- персонажа будем загружать из ресурсов
- создавать будем в рандомной точке, максимальный разброс для этой точке зададим константой сразу внутри фабрики (и наплевать, что так неправильно)
- потом прокинем teamId в нашего персонажа, чтобы в дальнейшем в зависимости от него могли выбирать противника
- Создадим Actor’а, которого далее заполним возможными действиями персонажа
- Класс Character будет содержать данные персонажа и выполнять методы для движения и атаки (немного перегруженный, но в контексте статьи мне это не важно)
- Зарегистрируем персонажа в Repository<Character>, а Actor’a в Repository<Actor>
3. Напишем персонажа
Теперь перейдем к классу персонажа. Я сразу напишу в нем все что понадобится нашим будущим правилам, а потом перейду к написанию ИИ. Следующий пример кода еще 10 раз можно переделать и разделить на меньшие по их ответственностям. Например, отдельный компонент для движения, другой держит ссылку на противника, третий для атаки, четвертый для получения урона. Таким образом каждое правило смогло бы работать с меньшим контекстом персонажа и быть более переиспользуемым. Но это опять же не тема статьи. В ней все что умеет делать персонаж будет лежать в одном огромном (не таком уж и огромном, всего 60 строк) классе:
4. Правила
Теперь когда наш персонаж готов, начнем писать первое правило - оно будет выбирать подходящего противника. То есть, правило просто пойдет в репозиторий с персонажами и найдет там самого ближайшего:
Следующее правило будет двигать персонажа к выбранному противнику, при наличии последнего:
Если персонаж достаточно близко к противнику и не находится в кулдауне атаки, то выполняется следующее правило:
И если здоровье игрока равно нулю, то следующее правило его уничтожит и сотрет из всех репозиториев:
Все что нам остается сделать это нацепить скрипт Character на префаб с персонажем, положить на сцену пустой объект со скриптом EntryPoint и готово.
5. Красивости
Также красоты ради напишем небольшой скрипт, который будет отображать команду персонажа, его очки здоровья и сам факт нанесения урона (не забудьте импортировать пакет DoTween для анимации получения урона в методе HandleDamage()):
После запуска по нажатию клавиш 0 и 1, будут создаваться персонажи. При достаточно активном нажатии, картина будет похожа на это:
В продолжение разговора о расширении этого фреймворка, то у нас довольно много неплохих вариантов, которые бы ускорили разработку новых правил для персонажей и сделали бы их более читаемыми:
- сделать обобщенный абстрактный класс Rule<T>, где T было бы контекстом, с которым бы работало правило. Тогда не пришлось бы в каждом правиле писать одинаковый конструктор с инициализацией поля Character
- сделать правило не stateless как сейчас, а хранящими состояние. То есть, внутри каждого правила мы бы определяли, вызывается оно первый раз, последующие или это вообще последний его вызов. Тогда можно будет добавлять определенные действия на старте или завершении действия правила
- наоборот в рамках оптимизации или создания более ECS-подобной архитектуры, можно было бы делать правила “чистыми”. То есть, контекст в них мы бы передавали не через конструктор, а напрямую в методы. Это бы означало, что нам не будет смысла плодить объекты правил для каждого персонажа. На нашем примере это не страшно, но представьте, если таких правил будет 15-20, а игроков одновременно на сцене 500-1000. То есть 20*1000 = 20000 лишних объектов для бедного сборщика мусора.
- добавить декораторы для правил: обертку, которая запускает несколько правил по очереди или запускает их параллельно
Но при каждом таком усовершенствовании в первую очередь надо будет задать себе вопрос: “А не пытаюсь ли я на самом деле реализовать какой-либо уже существующий фреймворк?”. Ведь декораторы взяты напрямую из Behavior Tree, а состояния в правилах - из State Machine. И нужно ли тебе на самом деле писать велосипед или лучше будет разом перейти на другой подход к написанию ИИ.
Подробнее об этом мы поговорим в следующей статье! Если кто захочет узнать еще больше о разработке, добро пожаловать на мой канал в телеграме и Youtube, на котором эта статья скоро выйдет в видео формате. Если кто-то захочет видеть все мои работы раньше остальных или просто поддержать, то добро пожаловать на Boosty!
Спасибо!
Инсайдер, которого ранее пытался заставить замолчать Ubisoft, вновь раскрыл новые подробности о Far Cry 7. Согласно свежей утечке, седьмая часть популярного шутера может отправить игроков в Новую Англию — регион на границе США и Канады. Первоначальные слухи 2023 года указывали на Аляску, но теперь источники утверждают, что действие развернется сред…
Сборы картины превысили 1,7 миллиарда долларов — при этом основную часть денег она получила на родине.
Одновременно рецепт практичной сборки и объяснялка, как правильно собирать игровой компьютер.
Компания уже ведёт переговоры с партнёрами и разработчиками игр.
Rule-Based AI (не путать Rule Based AI)
Две штуки названные совершенно одинаково но при этом совершенно разные?