В цикле статей «Особенности разработки мобильной MMO RTS» мы расскажем о работе большой команды над масштабным проектом Stormfall: Rise of Balur. Этот опыт будет полезен независимым разработчиками и студиям, которые еще не определились с выбором технологий, архитектуры и структуры команды для своей RTS.
![](https://habrastorage.org/r/w780q1/files/08a/0f4/689/08a0f46890734583a0673612ee35c32c.jpg)
Основным аргументом в пользу Unity был C#. Но есть и другие плюсы:
Теперь о плохом:
Если вы хотите прийти на рынок надолго, нужно хорошо подумать над архитектурой приложения. Если вы всё продумаете, сможете быстро и легко добавлять новые фичи и изменять старые. В сети есть куча статей по этой теме, но я хочу обратить внимание на несколько важных моментов:
Шаблон хорошо знаком WPF-разработчикам, и его суть в том, что при разделении модели данных от представления используется «связывание данных». Модель, как и MVC, представляет собой фундаментальные данные приложения и различные механизмы их обработки. Представление – это объекты графического интерфейса. Они являются подписчиками на события изменений значений свойств, которые предоставляются Моделью представления. Модель представления – агрегация необходимых для представления данных из модели. Она содержит команды, через которые представление может влиять на модель.
Из-за особенностей нашего приложения мы выбрали архитектурный шаблон MVVM. В отличии от MVC/MVP, он обеспечивает более высокий уровень абстрагирования UI от логики и данных, с которыми UI работает.
Модель в нашем приложении – это расшаренные с сервером классы с данными, механизмы их обработки и команды. Данные группируются по назначению в классах, которые также дают методы для доступа и обработки. Всё это предоставляется через фасадный объект для доступа из ViewModel.
![](https://habrastorage.org/r/w1560/files/0ad/b82/719/0adb82719d5d488697524cd5f8b15d91.png)
Команды являются единственными механизмами, через которые представление может влиять на Модель. Они представляют собой абстракцию для совершения операций, которая изменяет локальную модель, а также инкапсулирует логику синхронизации данных с сервером. Все команды являются обертками над HttpWebRequest и выполняются асинхронно (Asynchronous Programming Model). Для WebGL-билда команды являются обертками над Unity WWW классом, который выполняется через корутины. Для коммуникации с сервером данные сериализуются в JSON-формат.
Из-за асинхронного выполнения колбеков команд в других потоках из ThreadPool, а также из-за механизма динамической актуализации модели, который выполняется в отдельном потоке, необходима синхронизация доступа к данным. Эта логика инкапсулирована в фасадном объекте доступа к модели, который я описал раньше.
![](https://habrastorage.org/r/w1560/files/dc8/97d/e0e/dc897de0e61043a8bf4267e54ec7a0c1.png)
Слой ViewModel нашего приложения является самым объемным по количеству кода. По сути вся основная разработка фич происходит на этом уровне. На этом слое данные из разрозненных объектов модели собираются вместе для того, чтобы быть представленными пользователю во View. ViewModel никак не завязана на реализацию View, но сам набор и формат данных напрямую зависит от того, как именно они будут представлены в UI. Также на этом слое реализованы различные механизмы, которые могут не иметь UI, но необходимы для функционирования приложения: различные менеджеры для работы с социальными сетями и прочее.
Наша ViewModel оперирует несколькими базовых понятиями, среди них Property и Context. Property – это кастомная generic реализация паттерна ObservableObject. Контексты выступают в качестве контейнеров для Property и других Context. Context так же инкапсулирует логику поиска пропертей и логику активации и деактивации контекстов. Это необходимо в качестве оптимизации, чтобы контексты объектов, которые в UI, например, перекрыты чем-то, не ловили события и лишний раз не обновлялись. Механизм поиска у нас реализован через рефлексию и работает только в момент, когда какой-то UI элемент хочет забиндиться на Property из ViewModel и является далеко не самым узким местом по производительности.
Слой View отвечает за UI. Именно на этом уровне коду становится известно, что он работает в Unity. Группы объектов на этом уровне представлены:
Реализованный у нас механизм биндингов работает так:
![](https://habrastorage.org/r/w1560/files/eeb/f20/904/eebf20904656438984d1b4cd702024b1.png)
Пока всё. Во второй части поговорим о многопоточности, работе со скинами, выполнении запросов и их кэшировании.
![](https://habrastorage.org/files/08a/0f4/689/08a0f46890734583a0673612ee35c32c.jpg)
Выбор Unity. Преимущества и недостатки
Основным аргументом в пользу Unity был C#. Но есть и другие плюсы:
- В Unity достаточно низкий порог входа для разработчиков.
- Unity берет на себя все вопросы, связанные с совместимостью и корректной работой приложения на разных платформах. Нам только нужно убедиться в работоспособности билда в Unity Editor, проверить его на ключевых девайсах, а затем контролировать работу приложения через службы краш-репортов и наших Community Managers. Мы оповещаем о проблемах ребят из Unity, они это фиксят регулярными патч-релизами, а мы после этого выпускаем хот-фикс.
- Unity обеспечивают высокий уровень поддержки разработчиков. Стандартные ответы от агентов – редкость. Если вопрос содержит подробное описание проблемы, скриншоты и тестовый проект для запуска, вы получите исчерпывающий ответ с советами по выходу из ситуации.
Теперь о плохом:
- Низкий порог входа имел свои недостатки. Соискатели, которых мы собеседовали, неплохо знали Unity, но плохо – C#. Мы пришли к выводу, что лучше искать разработчиков с глубоким пониманием C# и желанием изучить самый популярный игровой движок.
- Нестабильное качество релизов. Особенно это касается патчей. Unity советует ставить их только тогда, когда они исправляют баг, затрагивающий ваш проект. Но что-то может сломаться в другом месте.
- Странные приоритеты в Roadmap. Например, долгие исследования полиморфной сериализации или вложенных префабов. Я думаю, что Unity пытается догнать своих ближайших конкурентов по качеству графики и не реализует фичи, которые очень сильно упростили бы разработку крупных проектов.
- Закрытая платформа. При возникновении проблем, решение которых зависит от Unity, у вас нет других вариантов, кроме как ждать нужного релиза.
Архитектура
Если вы хотите прийти на рынок надолго, нужно хорошо подумать над архитектурой приложения. Если вы всё продумаете, сможете быстро и легко добавлять новые фичи и изменять старые. В сети есть куча статей по этой теме, но я хочу обратить внимание на несколько важных моментов:
- Выберите шаблон архитектуры на начальном этапе разработки и всегда придерживайтесь его. Для Unity-проектов это нужно, чтобы понять, как именно будет происходить обмен данными между UI и бизнес-логикой. Ваша задача сделать обмен однотипным, понятным и прозрачным.
- Не пытайтесь описать архитектуру целиком до начала проекта. Она начинает вырисовываться только на стадии разработки. Для старта достаточно следовать принципам выбранного шаблона и постепенно оформлять похожие механизмы в виде архитектурных решений. Но всё же на начальном этапе разработки архитектуре стоит уделять больше внимания, чем созданию новой функциональности.
- Закладывайте в сроки время на проведения рефакторинга для внедрения новых архитектурных решений.
- Создавайте ограничения на работу в коде, просто обсуждений недостаточно. Вы договариваетесь с командой, что какой-то объект можно использовать только определенным образом, из какого-то потока или для специфических условий. Проходит пару недель, и кто-то обязательно принимается использовать этот объект не там где нужно и не тем способом, о котором договаривались, что зачастую приводит к сложным для определения проблемам.
- Придерживайтесь принципов SOLID, но без фанатизма. Здравый смысл никто не отменял. Представьте, что у вас есть 2 пути. Первый – реализовать продуманное модульное техническое решение, которое легко расширяется в любом месте. Второй – выполнить бизнес-задачи в ограниченные сроки, но тогда вся красота технического решения «по барабану». В этом случае выбирайте второй путь. Не опускайтесь до разработки ради разработки.
- Принимайте важные решения вместе с командой. Попытайтесь для обсуждения предоставлять несколько вариантов с плюсами и минусами каждого из подходов.
Почему MVVM
Шаблон хорошо знаком WPF-разработчикам, и его суть в том, что при разделении модели данных от представления используется «связывание данных». Модель, как и MVC, представляет собой фундаментальные данные приложения и различные механизмы их обработки. Представление – это объекты графического интерфейса. Они являются подписчиками на события изменений значений свойств, которые предоставляются Моделью представления. Модель представления – агрегация необходимых для представления данных из модели. Она содержит команды, через которые представление может влиять на модель.
Из-за особенностей нашего приложения мы выбрали архитектурный шаблон MVVM. В отличии от MVC/MVP, он обеспечивает более высокий уровень абстрагирования UI от логики и данных, с которыми UI работает.
Model
Модель в нашем приложении – это расшаренные с сервером классы с данными, механизмы их обработки и команды. Данные группируются по назначению в классах, которые также дают методы для доступа и обработки. Всё это предоставляется через фасадный объект для доступа из ViewModel.
![](https://habrastorage.org/files/0ad/b82/719/0adb82719d5d488697524cd5f8b15d91.png)
Команды являются единственными механизмами, через которые представление может влиять на Модель. Они представляют собой абстракцию для совершения операций, которая изменяет локальную модель, а также инкапсулирует логику синхронизации данных с сервером. Все команды являются обертками над HttpWebRequest и выполняются асинхронно (Asynchronous Programming Model). Для WebGL-билда команды являются обертками над Unity WWW классом, который выполняется через корутины. Для коммуникации с сервером данные сериализуются в JSON-формат.
Из-за асинхронного выполнения колбеков команд в других потоках из ThreadPool, а также из-за механизма динамической актуализации модели, который выполняется в отдельном потоке, необходима синхронизация доступа к данным. Эта логика инкапсулирована в фасадном объекте доступа к модели, который я описал раньше.
ViewModel
![](https://habrastorage.org/files/dc8/97d/e0e/dc897de0e61043a8bf4267e54ec7a0c1.png)
Слой ViewModel нашего приложения является самым объемным по количеству кода. По сути вся основная разработка фич происходит на этом уровне. На этом слое данные из разрозненных объектов модели собираются вместе для того, чтобы быть представленными пользователю во View. ViewModel никак не завязана на реализацию View, но сам набор и формат данных напрямую зависит от того, как именно они будут представлены в UI. Также на этом слое реализованы различные механизмы, которые могут не иметь UI, но необходимы для функционирования приложения: различные менеджеры для работы с социальными сетями и прочее.
Наша ViewModel оперирует несколькими базовых понятиями, среди них Property и Context. Property – это кастомная generic реализация паттерна ObservableObject. Контексты выступают в качестве контейнеров для Property и других Context. Context так же инкапсулирует логику поиска пропертей и логику активации и деактивации контекстов. Это необходимо в качестве оптимизации, чтобы контексты объектов, которые в UI, например, перекрыты чем-то, не ловили события и лишний раз не обновлялись. Механизм поиска у нас реализован через рефлексию и работает только в момент, когда какой-то UI элемент хочет забиндиться на Property из ViewModel и является далеко не самым узким местом по производительности.
View
Слой View отвечает за UI. Именно на этом уровне коду становится известно, что он работает в Unity. Группы объектов на этом уровне представлены:
- Механизмом биндингов.
- Объектами ContextBox – скриптами MonoBehaviour, которые являются базовыми для всех объектов, которые планируют использовать ViewModel, так как создают и контролируют жизненный цикл контекстов из ViewModel.
- Кастомными компонентами Unity, необходимыми для геймплея.
- UI-скриптами, например NGUI или Unity UI.
Реализованный у нас механизм биндингов работает так:
- На GameObject вешается UI-скрипт Label.
- На тот же GameObject вешается скрипт LabelBinding, он параметром принимает ссылку на Label, c которой и работает. Далее указывается путь к Property в ViewModel в строковом виде, с которой GameObject должен связаться.
- При Awake биндинг через ContextBox ищет в Context нужную ему Property по пути и, если находит, подписывается на её изменения.
- При изменении значения Property во ViewModel UI тут же реагирует на эти изменения и отображает их в ассоциированной Label.
![](https://habrastorage.org/files/eeb/f20/904/eebf20904656438984d1b4cd702024b1.png)
Пока всё. Во второй части поговорим о многопоточности, работе со скинами, выполнении запросов и их кэшировании.