Все стратегии хороши лишь до того момента, пока не выпущена первая стрела. Мэт Коутон. Огни Небес. Роберт Джордан.
Продолжаем наш
цикл посвящённый шаблонам проектирования и поговорим про очень полезный шаблон под названием Стратегия.
Теория
Этот шаблон позволяет менять поведение объекта в рантайме, а также он хорошо подходит для выделения похожих алгоритмов в отдельные объекты и их последующем переиспользовании.
Теоретическая часть шаблонов описывается очень часто, так что за ней можно обратиться к этой замечательной статье.
Практика
В качестве практического примера можно привести поведение искусственного интеллекта юнитов в стратегических играх.
По сути, поведение состоит из набора возможных действий, взаимосвязь которых часто описывают при помощи состояний, другого шаблона, о котором можно прочитать в другой нашей статье "Шаблон Состояние". Каждое такое действие, в свою очередь, представляет собой последовательность шагов:
Проверка на возможность выполнения действия - если это выстрел из пистолета, то проверяются наличие патронов и расстояние до цели. Для лечения проверяется доступность аптечки и текущий уровень хитпоинтов
Вводная анимация - юнит поднимает руку с пистолетом для выстрела или достаёт аптечку для лечения
Действие - выстрел(создание летящей пули или снятие хитпоинтов с противника) в случае с пистолетом и начисление хитпоинтов в случае с лечением
Выходная анимация - юнит опускает руку с оружием или прячет аптечку, после лечения
В результате эти два примера с выстрелом и лечением можно преобразовать в несколько отдельных стратегий:
Действие - высокоуровневая стратегия, описывает последовательность проверка-анимация-действие-анимация
Проверка наличия цели - она должна быть на подходящем расстоянии до выстрела и т.п.
Проверка хитпоинтов - не стоит лечить себя, без необходимости
Выстрел - если в игре есть выстрелы, то они будут не сильно различаться между собой
Лечение - также довольно однотипная механика
Это не полный набор стратегий для описания примера, но смысл должен быть понятен. Если подходить подобным образом к описанию всех возможностей поведения юнитов, то получится довольно ограниченный набор стратегий, который можно очень гибко настроить с помощью параметров. В результате это позволит сильно увеличить переиспользование кода, а следовательно, сократить объём багов и трудозатрат.
Реализация
Стратегии реализуется через описание их интерфейса, его имплементации вариантами стратегий, поддержкой механизма определения конкретной стратегии и её вызова через интерфейс управляющим объектом.
Для примера была реализована небольшая механика с магом, запускающим два разных огненных заклинания в вора. Я не буду приводить полную реализацию, так как по большей части она состоит из настройки анимации и выбора ассетов. А также попытка повторить этот небольшой пример с нуля самостоятельно будет хорошей практикой по работе с Unity и ресурсами.
Выбор заклинания происходит при помощи Immediate Mode GUI (IMGUI), подробнее о его применении можно почитать в нашей статье про чит панели. Работа с анимациями производится через Mecanim, базовую информацию о котором можно подчерпнуть из другой статьи "Шаблон Состояние".
В качестве интерфейса используется абстрактный класс:
usingUnityEngine;publicabstractclassSpell{// Триггер для запуска анимации
protectedintTriggerName;// Префаб заклинания
protectedGameObjectPrefab;// Место спауна префаба зависит от типа заклинания
protectedVector3ProjectileOffset;publicvoidFire(Vector3magePosition){// Объект заклинания создаётся с учётом сдвига точки появления
Object.Instantiate(Prefab,magePosition+ProjectileOffset,Quaternion.identity);}publicintGetAnimationTriggerName(){// Информация об анимации для данного типа заклинания
returnTriggerName;}}
Стратегии в нашем случае будут отвечать за вид и оффсета генерируемого объекта заклинания, а также триггер анимации:
Базовое заклинание
1
2
3
4
5
6
7
8
9
10
11
12
13
usingUnityEngine;publicclassBasicSpell:Spell{publicBasicSpell(GameObjectprefab){Prefab=prefab;// Сдвиг относительно положения мага
ProjectileOffset=newVector3(0.6f,0,0);// Тригер анимации базового заклинания
TriggerName=Animator.StringToHash("Attack");}}
Продвинутое заклинание
1
2
3
4
5
6
7
8
9
10
11
12
13
usingUnityEngine;publicclassAdvancedSpell:Spell{publicAdvancedSpell(GameObjectprefab){Prefab=prefab;// Сдвиг относительно положения мага
ProjectileOffset=newVector3(0.8f,0.2f,0);// Тригер анимации продвинутого заклинания
TriggerName=Animator.StringToHash("Attack_Extra");}}
usingUnityEngine;publicclassMage:MonoBehaviour{// Объект родитель для всех противников
publicTransformenemyList=null;// Префаб основного заклинания
publicGameObjectbasicSpellPrefab=null;// Префаб продвинутого заклинания
publicGameObjectadvancedSpellPrefab=null;// Анимация мага
privateAnimator_animator;// Флаг для контроля каста заклинания
publicstaticboolProjectileFlag=false;// Объект для текущего заклинания
privateSpell_spell=null;// Базовое заклинание
privateBasicSpell_basicSpell=null;// Продвинутое заклинание
privateAdvancedSpell_advancedSpell=null;// Настройки выбора заклинания
privateint_spellSelection=0;privatereadonlystring[]_spellNames={"Basic","Advanced"};voidStart(){_animator=GetComponent<Animator>();_basicSpell=newBasicSpell(basicSpellPrefab);_advancedSpell=newAdvancedSpell(advancedSpellPrefab);// Изначально выбрано базовое заклинание
_spell=_basicSpell;}voidUpdate(){// Если нет противников или уже есть скастованное заклинание
if(enemyList.childCount==0||ProjectileFlag){return;}// Запуск анимации мага для каста текущего заклинания
_animator.SetTrigger(_spell.GetAnimationTriggerName());ProjectileFlag=true;}privatevoidOnGUI(){// Прячем выбор заклинания во время каста и полёта текущего заклинания
if(ProjectileFlag){return;}// Отрисовываем панель выбора заклинания
GUI.Box(newRect(10,10,100,100),"Spell panel");_spellSelection=GUI.SelectionGrid(newRect(20,40,80,60),_spellSelection,_spellNames,1);// При отсутствии изменений ничего не делаем
if(!GUI.changed){return;}// При выборе базового заклинания делаем его активным
if(_spellSelection==0){_spell=_basicSpell;return;}// При выборе продвинутого заклинания делаем активным его
_spell=_advancedSpell;}// Создание текущего заклинания в подходящий момент из анимации каста
publicvoidFire(){_spell.Fire(transform.position);}}
Результат
В зависимости от выбранного заклинания маг запускает разные анимации заклинаний
Заключение
Сама по себе реализация шаблона стратегия не является чем-то сложным, по сути, это всего лишь использование наследования объектов. Самым большим вызовом станет выделение механик игры в структуру, которая была бы эффективна для конкретного проекта и скорее всего прежде, чем это случится, пройдёт несколько итераций рефакторинга, но чем раньше вы начнёте ответственно подходить к созданию этой структуры, тем проще вам будет поддерживать проект на достойном уровне. Пока! =)