В IT существует понятие паттерна, или шаблона проектирования — это пример удачного архитектурного решения, которое можно использовать снова. Такие шаблоны применяют при создании любой программы, и видеоигры не исключение.
Рассказываем, что такое паттерны проектирования и как их используют в геймдеве, вместе с Даниилом Ларжевским, Unity Software Engineer в DobroGames.
Что такое паттерны и зачем они нужны
Перед созданием приложения специалисты продумывают, как будет выглядеть его структура. Именно в этот момент применяются паттерны — своего рода «типовые проекты», по которым будет построена архитектура программы или ее частей.
Этот процесс можно сравнить с проектированием здания. Когда специалист рисует проект, он прибегает к устоявшимся в архитектурной среде шаблонам: какой должна быть крыша, по какой технологии обустроить фасад. Такие же шаблоны существуют и в разработке.
Основная задача паттернов — сделать процесс удобнее и быстрее. Они упрощают работу по многим причинам:
- готовое архитектурное решение использовать проще, чем продумывать с нуля;
- паттерны проверены временем и другими разработчиками, поэтому они предсказуемы — специалист знает, чего ожидать от поведения программы;
- приложения, построенные с использованием паттернов, легче поддерживать благодаря понятной всем структуре;
- общение с командой тоже упрощается — можно не объяснять решение, а просто назвать конкретный паттерн.
Шаблоны используют не всегда. Бывают ситуации, когда реализовать какой-то проект без них проще — например, когда нет подходящего шаблона или приложение слишком простое, чтобы использовать такие решения.
Можно провести аналогию: экскаватор помогает во время строительных работ, но никто не станет копать с его помощью грядки. Так и с паттернами: их стоит рассматривать как инструмент, полезный в определенных ситуациях.
Паттерны в разработке игр: есть ли особенности?
В геймдеве применяются те же паттерны проектирования, что в остальных сферах. Здесь нет каких-то ограничений. Геймплей и архитектура игр очень разнообразны, поэтому пригодиться может практически любой из существующих шаблонов. Например:
- если игра сетевая, понадобится паттерн для реализации модели «клиент-сервер»;
- если в игре можно набирать текст, нужны реализация простого текстового редактора и паттерн для таких решений;
- если присутствуют похожие друг на друга персонажи, например мобы, их можно создавать с помощью паттернов.
В зависимости от движка и конкретного проекта подход к дизайну внутренней системы игры может кардинально меняться. Но паттерны остаются универсальными для самых разных архитектур.
Какими бывают паттерны проектирования
В классическом понимании в разработке выделяют 22 паттерна проектирования. Они разделяются на три группы по назначению. Мы не будем описывать все возможные шаблоны — приведем только несколько основных паттернов проектирования из каждой группы.
Порождающие паттерны
Это шаблоны, которые помогают максимально удобно и безопасно создавать новые объекты. Например, генерировать персонажей, окружение и предметы. Вот два примера таких паттернов.
Singleton. Шаблон делает так, что у определенного класса есть только один экземпляр, доступный через одну общую точку. К нему могут обращаться любые объекты в программе. Обычно этот паттерн реализуют так:
- скрывают стандартный конструктор — метод, который создает объект класса;
- пишут статический метод, который создает единственный экземпляр объекта и возвращает его.
У паттерна есть минусы: он нарушает принцип единственной ответственности, потому что дает слишком много возможностей одной сущности. Класс не только выполняет свою функцию, но и сам принимает решение о создании своих экземпляров. Кроме того, если использовать паттерн часто, в проекте появится слишком много ссылок на его класс — а это затрудняет отладку.
В играх Singleton используют, когда нужно управлять единственным объектом, например главным персонажем в одиночной игре.
Builder. Этот паттерн помогает собирать объект по шагам, упрощая создание разных вариантов одного класса. В результате один код выполняет сразу несколько задач: например, генерирует разные версии одного и того же типа монстров на карте. При этом использование паттерна может усложнить код игры.
Существует похожий на Builder паттерн — фабрика, или Factory. Он тоже позволяет создавать разные объекты с помощью одного интерфейса. Разница в том, что Factory фокусируется на семействах объектов. Например, фабрика может создавать наборы кнопок и окон, которые выглядят одинаково в разных темах оформления.
Структурные паттерны
К ним относятся шаблоны, которые помогают настраивать иерархию разных объектов — делать ее более удобной и масштабируемой. Вот несколько примеров таких паттернов в программировании игр.
Adapter. Позволяет разделить зависимости между двумя классами за счет добавления прослойки — адаптера. Получается система, которая работает так:
- клиентский класс A обращается к адаптеру, чтобы получить результат от какого-то класса B, при этом ему совершенно не важно, как реализован класс B;
- адаптер получает информацию от класса B, переводит в вид, необходимый классу A, и возвращает нужные данные.
Из-за добавления классов адаптеров код программы может усложняться и раздуваться, поэтому использовать их стоит с умом. Тем не менее паттерн бывает полезным в геймдеве.
Например, в игре периодически появляется реклама и рекламные пакеты постоянно меняются. Чтобы не писать отдельный код для каждого, разработчикам удобнее создать заменяемый адаптер, через который игра будет обращаться к рекламе.
Composite. Компоновщик, или компонентный паттерн, объединяет объекты в древовидную структуру, где группы объектов ведут себя как единое целое. В такой системе можно одинаково работать как с отдельными элементами, так и с их объединениями. В итоге получается четкая иерархия: отдельные объекты образуют группы, а те — более крупные структуры.
В играх этот паттерн можно использовать при управлении группами объектов или персонажей. Например, к стаду коров можно обращаться так же, как к одной корове.
Поведенческие паттерны
Помогают создавать и настраивать отношения между разными объектами — то, как они ведут себя при взаимодействии друг с другом. В играх с их помощью можно реализовать разные геймплейные процессы и поведение персонажей.
Strategy. Паттерн для работы с разными способами выполнения одной задачи. В играх он полезен, когда нужно переключаться между разными вариантами действий. Чтобы добавить такую гибкость, используют шаблон:
- несколько вариантов логики выносят в отдельные классы;
- при необходимости, например при выполнении определенного условия, один вариант переключается на другой.
При работе с паттерном есть сложность: клиентскому классу нужно знать подробности реализации классов с логикой, чтобы определить, какой из вариантов использовать в конкретный момент. Это создает дополнительные зависимости и делает код сложнее.
В игре паттерн можно использовать так. Допустим, в ней есть мини-карта, на которой для игрока прокладываются маршруты. Игрок может идти пешком или ехать — для этих двух случаев маршруты нужно просчитывать по-разному. Можно применить шаблон: создать два отдельных класса для прокладки маршрута пешком или на транспорте. В зависимости от статуса игрока мини-карта будет переключаться между этими двумя классами.
Observer. Этот шаблон позволяет одним объектам подписываться на другие и следить за их изменениями. Если в наблюдаемой сущности что-то произойдет, объект-наблюдатель может отреагировать на ее изменения. Вот как это работает:
- наблюдатели подписываются на субъекта — наблюдаемую сущность;
- когда в субъекте происходит какое-то действие, наблюдатели получают об этом уведомление;
- для каждого наблюдателя можно прописать реакцию на то или иное событие.
В играх паттерн Observer можно использовать для взаимодействий между игроком и окружением. Например, за персонажем могут «наблюдать» другие: если он сделает что-то, что их разозлит, то они нападут.
Observer могут применять в связке с паттерном State, который описывает состояние объекта. Скажем, если состояние героя изменится с «ожидать» на «атаковать», наблюдатели тоже сменят свои состояния — на «убегать», «обороняться» или «атаковать».
Плюсы и минусы использования паттернов
Шаблоны используют не просто так. Это хорошо изученные предсказуемые решения, которые дают разработчикам ряд преимуществ:
- ускоряют и упрощают разработку;
- освобождают от необходимости создавать архитектуру с нуля;
- упрощают обновления и доработки продукта;
- повышают стабильность работы программ;
- помогают новым участникам команды быстрее понять проект.
Главная сложность при работе с паттернами — понять, какой из них лучше подойдет для конкретной задачи и нужен ли он вообще. Решение может быть очень субъективным и зависеть от предпочтений конкретного разработчика. А если в итоге выбрать неподходящий паттерн — разработка может замедлиться и усложниться.
Кроме того, у каждого паттерна есть свои минусы: раздувание кода, усложнение архитектуры и т. д. Разработчик должен о них помнить и учитывать при выборе.
Антипаттерны: чего не стоит делать
Антипаттерн — это обратное паттерну понятие. Он описывает ошибки и неудачные практики, которые мешают разработке и снижают качество продукта. Такие подходы универсальны и встречаются в разных сферах. Антипаттерны касаются не только архитектуры, но и стиля написания кода.
Выделяют пять основных групп антипаттернов.
Раздувальщики. Так называют участки кода, методы и классы, которые со временем чрезмерно разрастаются. А потому усложняют восприятие кода и дальнейшее развитие проекта. Если появился раздувальщик, разработчикам стоит остановиться и подумать, на какие более мелкие модули можно разбить код.
Нарушители объектного дизайна. Это решения, которые отходят от принципов объектно-ориентированного программирования. Примером могут быть чрезмерно сложные условия с большим количеством операторов if или switch. Еще одна проблема — повторяющийся код, который не вынесен в отдельные методы или классы. Такие подходы запутывают структуру и затрудняют дальнейшую поддержку программы.
Утяжелители изменений. Это различные проблемы, которые усложняют внесение правок в код. Их можно охарактеризовать как «одно починил — другое сломалось». То есть, чтобы исправить или дополнить часть программы, разработчику приходится менять еще один или несколько участков кода.
Замусориватели. В этой группе собраны в основном стилистические проблемы — код с ними может работать стабильно, но разобраться в нем тяжело. Возникают они из-за бесполезных участков, которые не выполняют никаких полезных функций. Например, к замусоривателям относятся непонятные комментарии или старый код, который остался после внесения изменений и уже давно не работает.
Опутыватели связями. Сюда относятся неудачные архитектурные решения, из-за которых между объектами появляется слишком много запутанных связей. Например, объекты, которые ссылаются друг на друга, делегируют все функции другим сущностям или используют данные других объектов чаще, чем собственные.
Где почитать про паттерны проектирования
Если вы хотите глубже изучить тему, можете посмотреть или почитать эти материалы. Они помогут разобраться в основах архитектуры любого проекта, в том числе игрового.
- Лекции Гарварда по основам программирования CS50 — цикл, который дает базовое представление об устройстве программ, алгоритмах и концепциях. В лекциях много теории, которая помогает глубже понять тему.
- Эрик Фримен, Кэтти Сьерра, Берт Бейтс, Элизабет Фримен. Паттерны проектирования — книга, в которой разобраны все основные шаблоны и даются примеры паттернов проектирования на языке Java.
- Рефакторинг.Гуру — сайт, посвященный рефакторингу и паттернам проектирования. Он работает как энциклопедия: для каждого паттерна есть своя страница с подробным описанием, схемой работы и примерами кода.
Краткие выводы
- Паттерны проектирования — это устоявшиеся практики, которые используют при создании архитектуры программы. Их применяют в любом направлении разработки, и геймдев не исключение.
- Паттерны делятся на три группы: порождающие — для создания объектов, структурные — для их организации, и поведенческие — для настройки связей между ними.
- Лучшие паттерны для игр выделить сложно — игровая архитектура бывает очень разнообразной. Часто применяются, например, порождающие паттерны Singleton и Builder, структурный паттерн Adapter и поведенческий Strategy.
- Использование паттернов ускоряет и упрощает разработку. Но если выбрать неподходящий шаблон — можно, наоборот, замедлить и усложнить процесс.
- Кроме паттернов, существуют антипаттерны — плохие практики, которые негативно влияют на качество кода и процесс разработки. Их стоит избегать, а если они появились в коде — подумать, как можно их устранить.