- PVSM.RU - https://www.pvsm.ru -
Недавно команда Whistling Kite Framework выпустила в релиз очередную игру, на этот раз — Змейку, написанную на Unity3D. Как и в большинстве игровых проектов, при решении вопроса о том, насколько детально нужно проектировать приложение, критическим фактором было время. В нашем случае причина проста: т.к. разработка велась в свободное от основной работы время, то идеальный подход к проектированию отложил бы релиз ещё на год. Поэтому, составив первоначальное разделение на модули, мы закончили проектирование и приступили к разработке. Под катом описание того, что из этого получилось, а также пара уроков, которые я вынес для себя.
Осторожно, картинки!
Сразу хочу вставить оговорку: все описанное ниже является лишь ретроспективой событий и попыткой анализа, что получилось хорошо, а что плохо. Статья не претендует на то, что в ней изложена непреложная истина, скорее даже наоборот — “вредные советы”, которые, возможно, уберегут кого-то от таких же ошибок.
Вначале пара слов, собственно, о приложении, чтобы было понятно, что именно мы проектировали и разрабатывали. Создавая змейку, мы постарались воссоздать именно старую добрую классическую змейку, в которой не было ничего кроме змеи, яблок, стен и бесконечного желания двигаться дальше. Именно поэтому мы сосредоточились на классическом геймплее, заведомо исключив из него на данном этапе все дополнительные возможности.
Одно из главных преимуществ нашей змейки — это разнообразные варианты управления, которые вы можете найти в настройках. Мы постарались предусмотреть их все:
Планируем добавить еще два:
Набранный в партии рейтинг записывается в таблицу рекордов. Рекордом можно поделиться с друзьями через смс, почту или любую социальную сеть, подключенную на смартфоне — твиттер, вконтакте, фейсбук и т.п.
Решение о старте разработки змейки мы принимали, преследуя две цели:
Первый, концептуальный, вариант архитектуры создавался нами ещё до выбора unity. Он приведен на картинке ниже. В этом варианте мы выделили слои будущего приложения:
Исторически последним был выделен слой инициализации, хотя в приложении он должен был запускаться первым. Дело в том, что мы сначала заложили инициализацию объектов в сами объекты, но потом решили выделить ее централизованно.
Слои интерфейса, логики и контроллеров представляют собой почти классической паттерн MVC. При этом было желание разделить обработку этих слоёв даже на разные потоки, чтобы обеспечить максимальную плавность интерфейса.
Слой управления был вынесен отдельно, т.к. мы решили, что разнообразные варианты управления будут одной из наших основных фич, поэтому нужно было делать так, чтобы всё остальное приложение не зависело от выбранного варианта.
Все слои должны были общаться между собой посредством выделенных интерфейсов, каждый слой содержал свои объекты и выполнял свои функции, зачастую содержал свой поток обработки.
Хорошие идеи:
Промахи:
Следующим этапом стал выбор платформы, хотя, строго говоря, выбор делался между unity и разработкой на чистой java. Беглый обзор других существующих движков не вызвал энтузиазма в их изучении. Мы пришли к достаточно ожидаемому выводу: написать змейку проще вообще без платформы, к тому же это выглядело интереснее — кто ж не любит делать свои велосипеды? Однако, мы выбрали unity, с целью ознакомления с платформой, близкой к статусу стандарта де-факто в области гейм-дева. Да, мы получили солидный оверхед из-за того, что unity — трёхмерный движок (на момент начала разработки у unity еще не было нативной поддержки 2D), а делали мы двумерную игру, но полученный опыт того стоил.
Пока выбирали платформу, мы совместными усилиями писали дизайн-документ, и уже на его основе родился второй проект архитектуры, заточенный под выбранную платформу.
Эта архитектура состояла из нескольких связанных диаграмм, которые я ниже называю терминами uml, хотя, конечно, стандарту они следуют, мягко говоря, не полностью.
Сущность-связь (понятно, что это скорее относится к разряду требований к системе, чем к её архитектуре, но в контексте статьи я не могу её не упомянуть)
Нотация ERD, думаю, всем знакома. На ней представлены все объекты игровой логики и логические связи между ними. Каждый такой объект порождает один класс, обеспечивающий работу этого объекта.
Диаграммы компонентов и кооперации. Для их восприятия предварительно опишу ряд договоренностей:
Общая архитектура — носит общий концептуальный характер, являясь, по сути, продолжением диаграммы этапа 1. На ней отображены:
Детализирующие диаграммы содержат:
В целом концепция изменилась не сильно: также есть отдельный компонент для инициализации, далее выделены две основные подсистемы.
После запуска приложения первым начинает работать компонент инициализации: он запрашивает у хранилища все необходимые данные и инициирует подсистему GUI.
Подсистема GUI должна обеспечивать работу пользователя с главным меню, настройками и рекордами. При старте игры, контроль передаётся на подсистему управления.
Подсистема управления должна обеспечивать взаимодействие пользователя с игровым миром, в первую очередь это — управление непосредственно змейкой.
Отдельно выделены компоненты для логирования и расшаривания рекордов.
Игровой мир состоит из объектов, взятых с ERD. Ключевую роль играют партия и змейка.
Партия хранит ссылки на змейку и текущий экземпляр фрукта, обеспечивает инициализацию игровых объектов и их взаимодействие.
Змейка обрабатывает основные игровые события: собственное движение, поедание фрукта и столкновения со стенами или хвостом.
Экземпляр фрукта отвечает за контроль собственного времени жизни.
Хорошие идеи:
Промахи:
После старта разработки архитектуру уже никто не корректировал. Я только оставлял отдельные заметки в соответствующем разделе дизайн-документа о том, как реализованы те или иные узкие места, но основная цель была выпустить релиз. Что ж, этой цели мы достигли, и я сел за рефакторинг, чтобы подготовиться к разработке второго релиза. Уже на тот момент я понимал, что в первую очередь надо переводить игру на честный 2d, поддержка которого как раз появилась в unity. И ещё было понимание, что полученный результат далёк от идеального, в первую очередь в части разделения функций по объектам и их взаимодействию, поэтому, я поставил себе две задачи:
На основе этой информации я планировал получить список того, что нужно менять. Результаты такого реверс-инжиниринга представлены ниже, в каждом разделе я сначала кратко описываю, о чем этот раздел, потом привожу одну или несколько диаграмм, иллюстрирующих построенное решение, а потом детально описываю, что и как было сделано.
На диаграмме ниже представлены все классы, созданные при разработке игры. На диаграмме скрыта часть методов, не несущих смысловой нагрузки или настолько тривиальных, что не стоят упоминания. Также часто под именем переменной (например, textures) скрывается целый блок переменных (в данном случае — разнообразных текстур).
Все имеющиеся классы разделены по четырем пакетам:
В пакете игровой логики центральное место занимает класс party, представляющий собой одну игровую партию. Этот класс отвечает за начало и окончание игры, за начисление рейтинга, за координацию событий поедания яблока и истечения времени жизни яблока и много других вспомогательных функций. Пока писалась
эта статья, я практически переписал этот класс, оставив в нем только вызовы методов из других классов (собственно, змейки и яблока). Это больше соответствует требованиям инкапсуляции, но сделало этот класс еще более похожим на контроллер в терминах MVC.
Второй по важности класс Snake содержит логику движения змеи, включая управление зависимыми объектами класса Snakechain. Именно этому классу передают команды контроллеры управления.
Для создания экземпляров фруктов (FruitInstance) используется фабрика, т.к. в будущем планируется увеличивать количество разных типов фруктов.
Пакет с интерфейсами содержит отдельные классы, отвечающие за отображение и обработку пользовательского интерфейса в зависимости от ситуации. Там же обрабатываются нажатия железных кнопок на устройстве.
Пакет с контроллерами содержит управляющие классы. Они добавляются к змейке и взаимодействуют напрямую с объектом Snake, используя паттерн command, а змейка выполняет поступившие команды в той же последовательности, но раздельно по своим шагам. Это было сделано для корректной обработки быстрых последовательных команд, например для разворота на 180 градусов вдоль своего хвоста.
Пакет с провайдерами содержит только один класс, интересный для данной статьи — это dataProvider. Этот класс содержит набор статичных функций, являющихся оберткой над вызовами стандартных методов для работы с хранимыми свойствами. На обоих предыдущих этапах проектирования работа с сохраненными данными предполагалась только однократно: загрузить их в память и больше не обращаться к более медленным носителям. Такой подход не учитывал вопрос независимости сцен в Unity: в результате, при переходе между сценами главного меню и игрового поля, все необходимые данные приходилось перечитывать заново.
Ниже приведено описание начального состояния двух сцен, из которых и состоит игры: сцены главного меню (на рисунке ниже слева) и сцены игрового поля (на рисунке ниже справа). Начальное состояние определяется тем, что было введено в редакторе, до запуска любого исполняемого кода. Именно из этих состояний начинаются большинство диаграмм последовательности ниже.
Главное меню создается двумя классами, Player и GUInavigator, присоединенными к единственному объекту на сцене, к камере. Player отвечает за загрузку всей основной информации об игроке, а GUInavigator инициирует нужное поведение из пакета интерфейсов и обеспечивает дальнейшие переходы между интерфейсами.
Игровая сцена содержит гораздо больше объектов. Большинство из них статические, представляющие собой мир змейки: фон, игровое поле, стены. Дополнительные поведения прикрепляются только к двумя объектам: к камере (GUInavigator, Player, party, fruitfabric) и голове змейки (Snake, Controllerselector). Controllerselector отвечает за выбор управляющего контроллера в соответствии с настройками игрока.
Диаграмма ниже отражает процедуру старта приложения (в той ее части, которая контролируется разработчиком). Особое внимание можно уделить разве что альтернативному выходу из сценария и переходу к загрузке игры, если есть сохраненная незавершенная партия.
Загрузка приложения состоит из обработки двух событий: awake и start. Событие awake обрабатывает объект player: в этот момент он обращается к DataProvider-у для загрузки информации об игроке, а потом вызывает собственный метод, отвечающий за применение текущих настроек, например, отключение звука.
Событие Start обрабатывается чуть посложнее:
Далее рассмотрим диаграмму последовательности, иллюстрирующую действия при запуске игры. Точка начала — загрузка сцены с игрой.
На данной диаграмме стоит отдельно отметить альтернативный сценарий загрузки сохраненной игры — это нужно для случая прерывания игры, например, входящим звонком.
Первыми срабатывают события Awake для объектов Player (загрузка всех настроек, аналогично старту приложения) и Snake (метод Init() — инициализиция хвоста “по умолчанию” из двух сегментов). Потом срабатывает событие Start для выбора контроллера (для примера использован FourButtonController), загрузки GUI и инициализации игры. Чуть подробнее об обработке этого события:
Игровая логика сосредоточена вокруг обработки двух событий: update обрабатывается в змейке методом movehead, здесь происходит движение змеи и управление движением ее сегментов; и ontriggerenter, где происходит обработка столкновений головы змеи с фруктами, стенами и собственным хвостом.
Разберем событие столкновения подробнее:
Движение же змейки реализовано следующим образом:
За рамки данной статьи осталось достаточно много логики: это логирование, как внутреннее, так и с использованием внешних средств аналитики; это работа навигатора интерфейсов, который инкапсулирует все особенности, связанные с двумя сценами; это работа с данными, с устройством, показ рекламы и еще много других особенностей. Если кому-то будет интересно, я могу написать об этом отдельно.
Основные выявленные проблемы:
Какие выводы для будущих проектов я сделал, исходя из всего вышесказанного?
Если вы видите еще какие-то не оптимальные решения, то прошу высказываться в комментариях, с удовольствием обсудим.
На всякий случай в конце привожу все ссылки, релевантные к статье:
P.S. За время подготовки данной статьи, часть найденных ошибок уже была исправлена, и, вероятно, были внесены новые, т.к. разработка не стоит на месте. :)
Автор: JSas
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/igry/68421
Ссылки в тексте:
[1] писать код: http://xn-----clcksaplxf6byd3cyb.xn--p1ai/
[2] Unity3D: http://unity3d.com
[3] yEd: http://www.yworks.com/en/products_yed_about.html
[4] DrawExpress Lite: https://play.google.com/store/apps/details?id=com.drawexpress.lite
[5] Visual Paradigm: http://www.visual-paradigm.com/
[6] play.google: https://play.google.com/store/apps/details?id=net.sf.wkf.snake&referrer=utm_source%3Dhabrahabr.ru%26utm_content%3Dsnake_article_2
[7] сайте-визитке: http://wkf.bit.ru
[8] Источник: http://habrahabr.ru/post/231087/
Нажмите здесь для печати.