Архитектура игры в Unity.
Я продолжаю работу над аркадным авиасимулятором. Сегодня речь пойдёт о базовых классах, обслуживающих функциональность игры.
Предисловие
Одна из глобальных целей моего текущего проекта — увереннее овладеть инструментами разработки. По этой причине я сознательно отказался от использования любых фреймворков (кроме Unity :D).
А ещё у меня нет опыта разработки в ООП, поэтому с паттернами проектирования я знаком лишь теоретически. С этой точки зрения для меня будет лучше написать спагетти-код, который я полностью понимаю, чем применять пресловутые паттерны, ценность которых я не смог прочувствовать.
Понятно, что это две крайности, между которыми нужно грамотно балансировать, но если мне предложат выбор между кривым, но рабочим кодом и красивой архитектурой, которую я не понимаю, я выберу первое.
Реализация
Unity я начал изучать по руководству "Unity in Action". И именно там я познакомился с паттерном "Локатор служб". По сути, это реестр сервисов, который занимается их инициализацией и раздаёт к ним доступ. За основу я решил взять именно его.
Root — MonoBehaviour, который содержит в себе поля-сервисы. Предполагается, что он будет являть собой точку входа, и плясать мы будем от него. В Awake() происходит заполнение последовательности инициализации сервисов, после чего запускается сама инициализация.
Сервисы, которые хранятся в данном реестре, — это объекты классов, реализующих интерфейс Service, у которого есть поле ServiceStatus status и метод Startup().
ServiceOfGameState — этот сервис сейчас отвечает только за паузу. Возможно, в будущем его юрисдикция будет расширена, но пока у меня нет для него других задач. Метод Service.Startup() у него пока пустой. Это повод задуматься над тем, нужен ли он тут в принципе, но пока я решил его оставить.
ServiceOfGameSettings — данный товарищ отвечает за хранение и изменение настроек игры. Пока это только настройки текущего языка. Установка и получение настроек предполагаются через публичные методы этого класса. В методе Service.Startup() происходит инициализация текущих настроек. Если это первый запуск — первичные настройки берутся из ScriptableObject-класса.
ServiceOfLocalization — сервис локализации даёт возможность гибко адаптировать игру под несколько языков. Пока только строки. Очень надеюсь, что в дальнейшем он будет отвечать за аудио реплики персонажей. Для прототипа достаточно текста. В инициализации мы обращаемся к сервису настроек — это значит, что на этот момент он уже должен быть проинициализирован.
ServiceOfUserInterface — на текущий момент это самый проработанный экземпляр. Отвечает за пользовательский интерфейс: отображение различных меню, обработку нажатия кнопок и так далее. При инициализации мы заполняем соответствия для элементов UI по их внутренним идентификаторам. Для view запоминаем их объекты MonoBehaviour, чтобы потом управлять видимостью отдельных окон. Для кнопок и списков выбора назначаем обработчики событий нажатия и выбора значения соответственно.
На диаграмме видно, что у этого сервиса нет публичных методов. Взаимодействие с этим сервисом я реализовал через подписку на Messenger.
Вообще, реализация интерфейса вызвала у меня больше всего сомнений, и я написал отдельный пост на эту тему, чтобы лучше в ней разобраться.
ServiceOfProgress - Этот сервис отвечает за хранение текущего прогресса игрока. К примеру, пройденные миссии и ачивки. Сейчас он достаточно прост и хранит текущий самолёт игрока и список всех доступных на данный момент самолётов.
Данный список не окончательный. Мы должны уметь управлять звуком, возможно, взаимодействовать с сетью, поэтому пара новых сервисов точно появится.
Сомнения
Service Locator
Я почитал несколько статей и, вообще, я совсем не уверен, что реализовал именно локатор служб :). Если я правильно понял, основная идея локатора служб — в возможности динамически добавлять в реестр классы, реализующие некий интерфейс (сервис).
В моём же случае инициализация происходит один раз при старте, а сам перечень сервисов поимённо приведён в полях класса.
В любом случае, мне нужна была точка входа и общий реестр всех необходимых для игры интерфейсов. Поэтому Root в текущем его виде меня полностью устраивает.
Singleton
Все сервисы, которые содержатся в Root, я сделал синглтонами, и я до конца не уверен в правильности этого решения. Для себя я это обосновал тем, что все основные сервисы нужны в единственном экземпляре.
Если у меня будет желание добавить сервис, который не получится сделать одиночкой, это будет явный сигнал о том, что ему не место в Root.
К примеру, объект самолёта. Во-первых, он не нужен на протяжении всего жизненного цикла игры, а во-вторых, объектов самолёта может быть несколько. Значит, на этом уровне абстракции ему не место.
P.S. Как я уже писал ранее, для меня важно, чтобы код в первую очередь работал. Ошибки проектирования, если они есть, так или иначе меня догонят. И я уверен, что шишек на этом набить ещё успею. Но опыт, вроде как, так и зарабатывается?
Если интересно, подписывайтесь на мой телеграм-канал. Там я регулярно делюсь прогрессом по своему проекту.