Применение шаблона проектирования программного обеспечения посетителя в разработке игр с использованием движка Unity3d

Абстрактный

Разработка игр как дисциплина сама по себе является сложной задачей по сравнению с традиционной разработкой программного обеспечения. Способность решать связанные с производительностью, архитектурные и другие проблемы часто является ключом к успеху в этой области. Из-за этих и многих других факторов часто рекомендуется следовать определенным принципам программного обеспечения и общепринятым практикам, чтобы обеспечить удобство сопровождения и расширяемость продукта. Шаблоны проектирования программного обеспечения пригодятся в таких сценариях. Они могут сделать кодовую базу более удобной в сопровождении, расширяемой и внести значительный вклад в общее время жизни продукта. Одним из вариантов использования, который необходимо изучить, является применение шаблона посетителя (Гамма и др., 1995) в контексте разработки игр. Возможные проблемы и преимущества реализации шаблона изложены соответственно в этом документе.

Предисловие

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

Это письмо является сугубо личной интерпретацией и видением вопроса, основанным на проблемах, с которыми я столкнулся при кодировании и попытке принять архитектуру на основе компонентов Unity3d.

Полный исходный код можно найти в моем репозитории GitHub: https://github.com/george-vasilchenko/unity-visitor.

Проблема

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

Настройка довольно проста. У нас есть абстрактный базовый класс с несколькими общими членами для персонажа. См. код класса CharacterBase:

Каждый из дочерних классов реализует необходимые члены соответствующим образом. Персонажи имеют набор характеристик по умолчанию, назначенный во время инициализации. Стартовый уровень у каждого персонажа 1. Для выполнения атаки по моему выдуманному сценарию важно рассчитать урон для атаки. Конечно, это можно реализовать сотнями способов, но в моем случае это будет расчет, основанный на взвешенном распределении характеристик конкретного персонажа. Вот реализация методов Attack и CalculateDamage из класса Archer:

Идея состоит в том, что у каждого персонажа, в зависимости от его типа (лучник, паладин и т. д.), есть своя ведущая черта. Лучник, например, имеет ловкость как свою главную черту. Напротив, маг использует интеллект в качестве основных характеристик. Эта тенденция реализована в методах CalculateDamage для каждого из персонажей.

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

Давайте рассмотрим другой сценарий для персонажей. Желаемой функцией может быть возможность повышения уровня в игре. Обычно такой функционал необходим. Способность повышать уровень в моем сценарии влияет на количество урона, которое может нанести персонаж. С каждым уровнем, как правило, персонаж будет наносить все больше и больше урона, это также имеет место в моем вымышленном сценарии. Рассмотрим следующую реализацию метода IncreaseLevel:

Мы увеличиваем переменную-член уровня при каждом вызове метода. Чтобы мы могли придумать правильное увеличение характеристик для данного уровня, мы должны увеличивать каждое свойство статистики каждый раз, когда уровень повышается. Используя метод CreateFromOtherWithDeltas структуры CharacterStats, мы получаем обновленный экземпляр статистики, созданный поверх существующего, и с дельтой для каждого свойства. Реализация структуры CharacterStats:

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

Направление, в котором движется эта реализация, очевидно. Мы продолжаем расширять класс с реализацией каждого нового уровня, кроме того, в более сложном сценарии разрешение значений характеристик было бы делегировано внешнему классу, который, в свою очередь, стал бы зависимостью для класса персонажа. И это повторяется для каждого нового персонажа, которого мы хотим добавить в игру. Это вызовет проблемы с обслуживанием, высокую сложность классов символов и тесную связь между возможными внешними модулями, предоставляющими числовые данные для этой логики.

Перспективное решение

Паттерн «Посетитель», согласно Гамме и др. (1995), предназначен для

«представляют собой операцию, выполняемую над элементами структуры объекта. Visitor позволяет определить новую операцию без изменения классов элементов, над которыми она работает».

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

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

Такие типы, как IDamageable и Enemy, являются просто дополнительными компонентами, чтобы сделать сценарий более или менее полным, они используются в методе Атака каждого персонажа. Я представил еще несколько типов, а именно ICharacterVisitor‹T› и две реализации этого интерфейса: StatsDistributionVisitor и StatsIncreasePerLevelVisitor. Первый был разработан, чтобы взять на себя ответственность за логику распределения, которая используется каждым персонажем определенным образом, а второй предназначен для решения проблемы статистики на основе уровней. Я переместил весь связанный код из классов персонажей в классы посетителей соответственно. Структура StatsDistribution была введена для инкапсуляции концепции дельты статистики. Код для посетителя раздачи:

Здесь мы видим, что значения, которые были предоставлены в методе CalculateDamage, перемещены в этот класс, и поддерживать эту структуру намного проще. Логика распределения статистики теперь находится в одном месте и может быть легко расширена любым внешним источником. Например, мы можем использовать экземпляр ScriptableObject (Unity, 2018) для управления этими значениями из редактора, что было бы хорошим решением для дизайнеров уровней и тестировщиков.

Второй класс посетителей отвечает за повышение характеристик каждого персонажа в зависимости от уровня. Вот реализация в коде:

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

Давайте посмотрим на фактическое использование классов посетителей, скажем, классом Mage:

Размер класса уменьшается. Количество логики, связанной с уровнем, больше не будет влиять на этот класс, потому что логика помещается в класс посетителя. Логика раздачи также заменяется вызовом метода Visit посетителя раздачи.

Тестирование

Чтобы опробовать обе реализации, я настроил несколько простых модульных тестов на основе редактора. Тесты заботятся о создании экземпляров персонажей. Каждый персонаж заставляет своего врага атаковать. Чтобы не усложнять сценарий и не отклоняться от темы, логика оценки просто проверяет изменение здоровья каждого врага. Кроме того, информация заносится в консоль, чтобы проиллюстрировать разницу. Тесты идентичны в отношении реализации до шаблона посетителя и после, единственная разница заключается в реализации классов символов. Вот пример кода для одного из тестов:

Результаты испытаний идентичны. Вот результат для реализации без шаблона посетителя:

Вот результат после реализации шаблона посетителя:

Мысли

Учитывая количество усилий, которое требуется для реализации игровой логики при разработке игр, очень важно найти эффективный способ сделать это. Использование шаблонов проектирования программного обеспечения считается хорошей практикой, но шаблоны должны применяться с разумной осторожностью. Часто возникает соблазн чрезмерно усложнить дизайн и реализацию, что в результате ставит под угрозу успех продукта. Подход к кодированию Unity3d основан на проектировании на основе компонентов (разработка программного обеспечения на основе компонентов, 2020 г.). Довольно сложно следовать принципам объектно-ориентированного программирования в такой среде, когда архитектура работает против вас. Однако в некоторых изолированных компонентах можно решить проблемы, следуя общим рекомендациям. Шаблон посетителя оказывается полезным в сценариях, когда нам приходится иметь дело с несколькими объектами одной и той же структуры, которые должны выполнять определенные операции. Посетитель позволяет нам объединять связанные операции, определяя их в одном классе (Гамма и др., 1995). Это открывает дополнительные возможности для использования расширенного набора функций движка, когда мы хотим влиять на данные классов посетителей.

Послесловие

В реализации шаблона посетителя отсутствует так называемый общий метод Apply. Это сделано намеренно и я считаю ненужным, потому что способ разрешения экземпляров классов посетителей удовлетворяет общей идее шаблона. Конечно, можно добавить еще один уровень абстракции в классы персонажей, такие как ArcherDamageSystem, ArcherStatsSystem, которые, в свою очередь, будут использовать классы посетителей и раскрывать Apply , но я считаю, что это не главное, и игнорирование метода Apply допустимо в силу обстоятельств.

использованная литература

Разработка программного обеспечения на основе компонентов. (2020, 29 июня). В Википедии. Получено с https://en.wikipedia.org/w/index.php?title=Component-based_software_engineering&action=history

Гамма, Эрих и др. Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения. Аддисон-Уэсли, 1995.

Технологии, Юнити. «Скриптируемый объект». Unity, 15 октября 2018 г., docs.unity3d.com/Manual/class-ScriptableObject.html.