Для чего нужно наследоваться от класса Health?
В игре есть пули, ракеты и лазеры, которые при попадании наносят урон. Так как у нас только один класс отвечает за разрушаемость, то нам достаточно проверить, есть ли скрипт Health у объекта, куда попала пуля. И если есть - нанести урон.
Сейчас хочу сказать, что общая структура противников получилась очень удобной и понятной. Если нам нужны были какие то дополнительные параметры для корабля, то мы уже создавали новый класс на базе SpaceShip (например, для авианосца нужна ссылка на объект-истребитель, а также два параметра - лимит истребителей и задержка между их запуском). Параметры корабля инициализировались в скрипте Генератора противников, перед выходом корабля из варпа.
Ну что, теперь у нас уже есть некоторые параметры для нашего ИИ.
На чем сделать ИИ?
Изначально, я очень хотел сделать ИИ на базе нейронных сетей, чтобы корабли сами выстраивали свою стратегию полета, уклонялись от снарядов и вообще, становились сильнее с каждой игрой. К сожалению, мне не хватило знаний, чтобы формализовать и перенести такие действия на нейронные сети. Хотя как работают нейронные сети и как проводить их обучение - я знаю :)
Раз нейронные сети отпадают, то оставался вариант - машина состояний (state machine). Такой вариант хорошо вписывался в общую концепцию поведения кораблей, потому что любое поведение кораблей можно было разделить на составляющие - подлететь к базе, отлететь от базы, приблизиться на определенную дистанцию, лететь к выбранной точке А, повернуться стороной и т.д.
Визуализация двух состояний истребителя
Оставалось выбрать, как реализовать машину состояний:
1. писать свой "движок" для состояний;
2. использовать какой то готовый "движок".
Перед тем, как что то выбрать, сначала надо изучить доступные варианты. В интернете мы нашли много различных готовых "движков", а также подробных инструкций, как сделать свой движок. Но при этом, мы нашли информацию, что в Unity уже есть встроенная машина состояний - это Аниматор. Изначально, задача аниматора в Unity - это проигрывание разных состояний анимации персонажа - бег, ходьба, атака и тд. Но при необходимости, можно добавить выполнение кода в состояние. Вот она, лазейка :)
Многие скажут, что написать движок для машины состояний легко, и будут правы! Игру мы пишем под мобилки, где как раз процессоры многоядерные.
Получается, мы можем писать логику ИИ практически без всякой нагрузки на главный процесс игры! Это же какая оптимизация получается! :) Только при разработке кода, нужно учитывать, что код выполняется параллельно, и если мы будем менять какие то общие переменные, то нужно делать это безопасно, с использованием блокировок.
Состояния аниматора Unity
Чтобы использовать код в состояниях, нужно написать скрипт, который наследуется от класса StateMachineBehaviour. Мы постарались сделать некий удобный шаблон, который использовали во всех подобных скриптах.
public class StateTemplate : StateMachineBehaviour
{
[Header("Parameters")]
//тут можно объявить всякие параметры
[Header("State settings")]
public string onExitDeactivate = "stateA";
public string onFinishActivate = "stateB"; //приватные переменные
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
//вызывается при каждом входе в состояние
//через animator.GetComponent можно получить доступ к скриптам объекта
}
public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
//пока состояние активно, постоянно вызывается
//если нужно выйти из состояния, вызываем animator.SetBool(onFinishActivate, true);
}
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
//вызывается при каждом выходе из состояния
animator.SetBool(onExitDeactivate, false);
}
}
Каждое состояние у нас связано с определенной переменной типа bool в аниматоре.
Когда нам нужно выйти из состояния А, мы активируем переменную stateB, и у нас состояние переходит в состояние B. При этом, переменная stateA сразу деактивируется. Функция OnStateUpdate вызывается либо каждый кадр, либо каждый "физический тик", в зависимости от настроек аниматора.
В функции OnStateEnter обычно кешируются различные классы противника, проводится инициализация состояния, рассчитываются точки маршрута и т.д.
Также важно учесть, что внутри функций нельзя объявлять переменные, иначе при параллельной работе значения будут перезатираться. Все переменные объявляются как атрибуты класса и будут принадлежать одному противнику (владельцу аниматора). Это особенность аниматора Unity.
Разработка ИИ
После этого начался творческий и достаточно сложный этап продумывания поведения для каждого корабля. Мы пересмотрели различные космические битвы, стрелялки с видом сверху и прочее. И придумали следующие стадии для каждого противника:
1. Истребители будут иметь два состояния - в одном состоянии они приближаются к станции на определенную дистанцию, а потом переходят во второе состояние и отлетают в случайном направлении и потом всё повторяется.
2. Фрегаты также имеют два состояния, сначала они приближаются к станции на некоторую дистанцию, а потом переходят во второе состояние, где летают влево-вправо перед станцией.
3. Крейсеры и Авианосцы также имеют два состояния - подлетают к станции и после этого поворачиваются одной из сторон к станции и открывают огонь.
В дальнейшем, мы улучшили поведение истребителей. Потому что изначально, каждый из них летал самостоятельно, кто то нападал, другой в это время отлетал. Получалось какое то месиво. Этот тип кораблей стал самым ненавистным в игре :) Мы хотели их как то сгруппировать, чтобы игроку было удобнее и зрелищнее их уничтожать. И тут мне попадается статья о симуляции полета стаи птиц под названием Boids. Увидев эту симуляцию, мы просто загорелись желанием реализовать подобное поведение у истребителей! Через несколько вечеров доработка была готова!
Теперь каждый истребитель присваивался группе. "Голова" группы выбирал куда лететь - на станцию или от станции, все остальные члены группы следовали за ним с помощью алгоритма Boids.
Состояния ИИ истребителя
Теперь у истребителей было три состояния:
1. Boids - если истребитель не "голова" группы, он остается в этом состоянии. Здесь происходит расчет движения корабля в стае. В случае, если текущая "голова" группы будет уничтожена, следующий за ним в группе корабль становится новой "головой" группы.
2. Fly Back - это состояние для "головы" группы, истребитель выбирает случайное направление и удаляется от станции.
3. Chase - это также состояние для "головы" группы, истребитель приближается к станции.
Демонстрация нового ИИ истребителей
Результаты оказались просто потрясающими! Новое поведение истребителей стало гораздо зрелищнее и приятнее :)
Если кому то будет интересно, как именно писать код для состояний, то вот пример скрипта, который случайно поворачивает Крейсер влево или вправо и открывает стрельбу по игроку NetRotateSide.cs
В завершение статьи хочу сказать, что разработка ИИ оказалась одной из самых интересных этапов написания игры. У нас получилось сделать гибкую систему, которую легко можно расширить и добавить новые фичи. Надеюсь, что статья поможет кому то с выбором основы под ИИ :)
В следующей статье я расскажу о том, как мы делали локализацию.
Если у Вас есть идеи поведения противников - делитесь!
Всем хорошего кодинга!
Предыдущие статьи:
Часть 4. Противники (продолжение)
Часть 3. Противники
Часть 2. Генератор волн противников
Часть 1. История разработки
Ссылка игры в Google Play: Space Turret: Defense Point