Введение В Компонентно-Ориентированное Программирование

Сам Unity Engine (далее Unity), как и многие другие игровые движки, наиболее подходит для компонентно-ориентированного программирования (далее COP), поскольку Поведенческий паттерн является одним из базовых паттернов архитектуры движка, наряду с паттерн «Компонент» из классификации Decoupling Patterns. Таким образом, компонент является базовой единицей реализации бизнес-логики в Unity. В этой статье я расскажу о том, как использовать COP в Unity. В целом ООП можно рассматривать как эволюцию принципов ООП, устраняющую проблемную область, известную как хрупкий базовый класс.

В свою очередь, сервис-ориентированное программирование можно считать развитием COP. Но вернемся к теме.

Стоит сразу отметить, что КОП-КОП, но никогда нельзя забывать о принципах GRASP, SOLID и других.

Мы будем называть компонент классом, унаследованным от MonoBehaviour. Здесь само имя базового класса выбрано очень удачно, дает понять, что это поведение, и говорит, что оно выполняется платформой .

mono. Но довольно удобно думать о MONO как об одном *одном* поведении.

И первое, на что нужно обратить внимание при разработке компонентов: один компонент — одно поведение.

Принцип единой ответственности от SOLID и высокая связанность методов внутри класса по GRASP в действии.

Теперь обратим внимание на GRASP. Программирование должно осуществляться на уровне абстракций, а не конкретных реализаций (Low Coupling).

И даже, казалось бы, простой игровой процесс требует создания диаграмм интерфейса UML. За что? Не излишне ли, не дает покоя принцип «ЯГНИ» (Вам это не понадобится).

Не чрезмерно, когда это оправдано.

Кто придумывает игру? Геймдизайнеры, эти коварные люди гражданской внешности.

И никогда не бывает, чтобы они что-то не меняли.

Поэтому нужно быть готовым к изменениям, но еще чаще нужно быть готовым к расширению игровой логики.

Причем все эти изменения будут внесены не сразу, а только тогда, когда сами разработчики забудут, почему они сделали именно так, а не иначе.

Поэтому UML-диаграммы, хотя бы абстракции, нужно делать всегда: это документация для проекта.

Я буду разрабатывать диаграммы UML в Visual Studio, чтобы затем генерировать на их основе код C#.

Итак, приступим к разработке игры; Например, давайте создадим основной игровой процесс для игры Tower Defense. Существуют разные подходы к этапам разработки, я буду использовать сокращенный вариант.



Первый шаг: постановка проблемы

Сколько бы многостраничной документации ни создавалось по игре, всегда нужно уметь выделить главное.

Примерное описание игрового процесса, конечно, лучше разделить на варианты использования: Игрок должен не дать врагам разрушить Дом.

Для этого он должен разместить в радиусе действия башни, которые будут уничтожать врагов.

Одна башня может одновременно атаковать только одного врага.

Когда он умирает или покидает зону действия башни, выбирается следующая доступная.

Враги появляются волнами, с каждой волной их количество увеличивается.

Враги движутся от точки появления по дороге к Дому.

Подойдя к дому, они начинают его разрушать.

Когда дом разрушен, игра окончена.



Второй шаг: анализ задачи

Начнём с работы над главным: башнями, крипами и домами.

Итак, создав сцену с уже расставленными игровыми объектами (башнями и домом), мы должны получить результат: крипы появляются, движутся в сторону дома, по пути умирают от урона башен, а когда доходят до дома, они причиняют ему вред. После смерти крип исчезает со сцены.

Если дом разрушен или все крипы убиты, игра окончена.

Выделим основные игровые сущности, укажем их свойства и поведение: 1) Башня.

Свойства: урон, время восстановления между выстрелами, радиус выбора цели, логика выбора цели.

Поведение: выбор цели и атака цели.

Также, чтобы разнообразить игровой процесс, будут разные типы башен: по урону, радиусу поражения и логике выбора цели.

2) Ползучесть.

Свойства: HP, урон, время восстановления между атаками, скорость передвижения.

Поведение: Движение по маршруту Домой.

Нападение на дом при близком приближении к нему.

Когда HP = 0, считается убитым.

Разные виды крипов - например, по ХП.

3) Дом.

Свойства: HP. Поведение: Когда HP = 0, игрок проиграл.



Третий шаг: разложение

Я считаю, что правильная декомпозиция задач очень важна.

Большую часть времени стоит потратить на проработку абстракций и логических связей между ними.

Начнем с обобщения поведения, чтобы определить, какими должны быть компоненты.

1) В игре есть две сущности, наносящие урон: башня и крип.

Давайте вынесем это поведение в компонент, вот его интерфейс:

Введение в компонентно-ориентированное программирование

Подождите, подождите, кто-то скажет: «А как насчет инкапсуляции? Значит, любой может изменить время восстановления и уронЭ» Нет, только гейм-дизайнер, интуитивно настраивающий свойства компонента.

Таким образом, все свойства будут доступны только для отображения информации в пользовательском интерфейсе, и никакой набор не потребуется.

Кроме того, Unity не сможет отображать свойства в инспекторе с помощью указанной конструкции get/set и потребуется создать поля с пометкой [SerializeField].

И это правильно, другие классы из кода не смогут изменить значение свойства.

Вернемся к компоненту.

Кто-то спровоцирует такое поведение и остановит его, уточнит цель, изменит цель.

Но кто? 2) Поскольку логика выбора цели будет разной для разных типов башен, и такое поведение будет использовать не только башня, но и крип, необходимо выделить две логические составляющие.

Первый будет работать с физикой, как только крип входит в триггерный коллайдер, он добавляет его в очередь целей, создавая событие, сообщающее, что было добавлено больше целей.

Как только крип покинет зону поражения, удалите эту цель из очереди, создайте событие удаления крипа из очереди.

Конечно, для дома такое поведение избыточно: дом никогда не убежит. В любом случае.

Назовем этот интерфейс будущего компонента ITrigger. С двумя методами Unity: OnTriggerEnter/OnTriggerExit.

Введение в компонентно-ориентированное программирование

Второй логический компонент будет реагировать на эти события и использовать сам IDamageDealer, чтобы нанести урон выбранной цели и остановить урон.



Введение в компонентно-ориентированное программирование

Но как же тогда заставить второй компонент выбирать цель по-разному, в зависимости от типа башни и от того, является ли она вообще крипом? Простой вариант — некий универсальный метод SelectTarget (Тип логики выбора цели (тип башни, крип)), в зависимости от типа логики выбора цели выбираем его.

Но универсальность не всегда хороша, особенно если речь идет о компонентах.

Тут на ум приходит принцип разделения интерфейсов: несколько конкретных лучше, чем один универсальный.

Поэтому будут разные компоненты для разного поведения выбора цели, объединенные одним интерфейсом ITargetSelector.

Введение в компонентно-ориентированное программирование

KeepSelector: выбирает дом в качестве цели.

SimpleCreepSelector: выбирает первую цель в списке.

WeakCreepSelector: выбирает самую слабую цель для добавления.

Таким образом можно легко расширить базовую механику выбора цели (механика является основной, поскольку башни отличаются только друг от друга).

Однако вы можете сделать другой вариант — с наследованием.

Будет базовый компонент TargetSelector с логикой по умолчанию для башни.

А классы KeepSelector и WeakCreepSelector переопределят метод добавления в список целей (чтобы проверить дом это или крип) и метод выбора цели.

3) В игре есть две сущности, которые имеют следующее поведение: при получении урона количество очков жизни уменьшается, пока они не умрут.

Введение в компонентно-ориентированное программирование

Зачем нужен IsDead, если можно просто проверить состояние HP<=0? Even in the current implementation, in two places it is necessary to check the condition to see if the creep has died. Therefore, following the simple principle of DRY (Do not repeat yourself), we will not duplicate the logic. This will make it easier to change the logic if any other conditions are added. Чтобы в будущем можно было легко расширить метод нанесения урона, дадим такое поведение другому вспомогательному компоненту:

Введение в компонентно-ориентированное программирование

Таким образом, мы сможем объединить два компонента, IHittable и IDamageDealer, в будущем без наследования или переопределения метода повреждения.

Это поведение также легко расширить, назначив другие компоненты, реализующие IDamageDealer. 4) Один объект имеет поведение навигации по маршруту: IRouteFollower. Свойства: WayPoints[], Скорость; 5) Теперь нам нужно определить компонент, который будет обрабатывать логику проигрыша и выигрыша: проверить IsDead дома и всех крипов с учетом волнового номера.



Введение в компонентно-ориентированное программирование

В логике реализации мы будем вызывать метод crypt spawn на Start, проверять в Update сколько крипов мертво из тех, что были заспавнены, и с учетом количества волн и текущей волны спавнить или говорить, что у игрока есть выиграл.

Также проверьте, разрушен ли дом: если да, то игрок проигрывает.

Четвертый шаг: реализация

Теперь вы можете реализовать первую версию игры.

Однако кто-то спросит: «А где сами классы Tower и CreepЭ» В Unity мы будем создавать префабы, настроенные как визуально (башни разных типов, крипы, дом), так и логически, то есть с добавлением компонентов.

Также необходимо настроить свойства компонентов, поэтому нет необходимости создавать отдельные классы.

Но нам нужно будет понять (для реакции в OnTriggerEnter/OnTriggerExit), что такое крип, а что такое дом.

Для этой цели в Unity есть теги.

Некоторых расстраивает, что на один игровой объект может быть только один тег, но это нормально: не следует делать универсальные объекты.

Начнем реализацию с генерации кода диаграммы UML и получения фиктивного скелета.

Затем мы создадим реализацию поведения компонента.

Помните, что лучше использовать абстракции, чем конкретные реализации, хотя это повысит согласованность.

Пример моей реализации можно увидеть здесь: https://github.com/sountex/COPTD Итак, мы сделали это.

Как мы можем теперь расширить игровой процесс — скажем, добавив экономику? Теперь вам нужно получить деньги за убийство склепа и потратить их на постройку башен.

Не спешите добавлять новые свойства в класс, реализующий одно из вариантов поведения ползучести.

Помните принцип единой ответственности класса и изолируйте это новое поведение в новом компоненте.

Создав новый компонент, поведение которого будет включать в себя хранение данных о сумме денег, внесенных за убийство, мы создадим лучшее решение, которое можно использовать повторно.

Немного о повторном использовании.

Например, в РТС - тоже есть юниты, наносящие урон и получающие его, здания.

Благодаря компонентно-ориентированному подходу и абстракции все созданные компоненты можно легко использовать в играх другого жанра.



Следующий шаг: тестирование

Для тестирования поведения игрового объекта как набора компонентов удобнее всего использовать BDD (Behavior-driven development).

А для тестирования компонентов отдельно Unit Test. Но это отдельная тема.

Теги: #unity3d #КОП #Разработка игр #Разработка сайтов #программирование #unity

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.