Выбор 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.
Команды являются единственными механизмами, через которые представление может влиять на Модель. Они представляют собой абстракцию для совершения операций, которая изменяет локальную модель, а также инкапсулирует логику синхронизации данных с сервером. Все команды являются обертками над HttpWebRequest и выполняются асинхронно (Asynchronous Programming Model). Для WebGL-билда команды являются обертками над Unity WWW классом, который выполняется через корутины. Для коммуникации с сервером данные сериализуются в JSON-формат.
Из-за асинхронного выполнения колбеков команд в других потоках из ThreadPool, а также из-за механизма динамической актуализации модели, который выполняется в отдельном потоке, необходима синхронизация доступа к данным. Эта логика инкапсулирована в фасадном объекте доступа к модели, который я описал раньше.
ViewModel
Слой 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.
Пока всё. Во второй части поговорим о многопоточности, работе со скинами, выполнении запросов и их кэшировании.
Комментарии (24)
Visteras
20.12.2016 11:22На самом деле интересно, и надеюсь что вторая часть действительно будет.
Плюс — интересно какой ЯП использовали для сервера?Plarium
20.12.2016 12:20+1На сервере используется С#, соответственно у нас есть возможность частично шарить код между сервером и клиентом.
Suvitruf
20.12.2016 12:38Оу. У нас в игре тоже сервер на Юнити, что позволяет очень много кода шарить с клиентом. А можете поделиться информацией о перформансе сервера?
Плюс, что более интересно, как управляете серверными инстансами? И т.п.
GLeBaTi
20.12.2016 11:22Если не затруднит, можно примеры кода?
Plarium
20.12.2016 12:21Примеры какого именно кода интересуют?
GLeBaTi
20.12.2016 13:12Пример вот этого:
Далее указывается путь к Property в ViewModel в строковом виде, с которой GameObject должен связаться.
При Awake биндинг через ContextBox ищет в Context нужную ему Property по пути и, если находит, подписывается на её изменения.
При изменении значения Property во ViewModel UI тут же реагирует на эти изменения и отображает их в ассоциированной Label.
Plarium
20.12.2016 19:02Если вас интересует реализация этого механизма, то показать мы его, к сожалению, не можем. Как и говорится в статье, для поиска необходимых Property используется рефлексия.
Пример использования Property в Context выглядит как-то так:
public class AlertPopupContext : Context<AlertPopupContextState> { public readonly StringProperty Message = new StringProperty(); protected override void Set() { Message.Set(State.Message); } }
В UnityEditor в биндинге, который лежит в иерархии префаба AlertPopup, путь задаётся так как на картинке ниже, в соответствии с именем переменной.
Leopotam
21.12.2016 00:01+1Те в случае вычисляемых полей при множественных зависимостях и при последовательном изменении значений этих зависимостей в одном обработчике у вас лейбл будет перезаписан несколько раз с соответствующими gc memory allocation на строках?
Plarium
21.12.2016 17:02Хорошее замечание, если мы вас правильно поняли. Чтоб этого избежать, мы собираем все изменения и выполняем только самое последнее в конце кадра.
Leopotam
21.12.2016 17:56А как производится вычисление составного поля? Например «прогресс 10 / 100», когда требуется несколько зависимых полей. Это решает дата-провайдер, получается, какую строку в какой виджет отдавать?
OlegGelezcov
20.12.2016 13:12Вы взаимойдествуете с сервером по протоколу http? Никаких сокет-соединений? Что ж это за реалтайм такой, опишите подробней?
Suvitruf
20.12.2016 13:46http с keep-alive, по сути, тоже самое, что и raw socket. Только лишних заголовков много. Если сообщения отправляются только в одну сторону, то вполне жизнеспособно.
Leopotam
20.12.2016 23:58WWW в юнити (а в статье указан именно он) раньше принудительно вырезал keep-alive и еще кучу заголовков, как сейчас — без понятия, но не думаю, что поведение сильно изменилось. Возможно новый вебреквест из HLAPI уже умеет такое.
Suvitruf
21.12.2016 00:25Мы для запросов к API используем UnityWebRequest с keep-alive. Работает отлично. Основное соединение с сервером через LLAPI. С WWW и других проблем много было. Благо сейчас есть хорошая замена.
Plarium
20.12.2016 14:45Да, на https с long polling для некоторых запросов. Одна из следующих статей в этом цикле будет посвящена как раз работе с сервером, там будет все расписано подробнее, так что следите за обновлениями :)
Suvitruf
Не совсем правда. У них можно попросить внутренний билд, если не боитесь. Плюс, у них разработчики некоторые идут на контакт с радостью, так что проблему можно прям с ними обсудить.
Plarium
Не только.
Как было сказано выше в статье, у нас с Unity партнерские отношения, мы работаем с техподдержкой уровня Enterprise. Однако, с внутренним билдом часто возникают технические сложности, поэтому нам проще подождать релиза.
Suvitruf
Вот и мы ждали-ждали, вышла 5.5.0, а там кое-какие вещи сломались. Заметили это уже после апдейта в сторах =/
KumoKairo
Напишите пожалуйста какие конкретно вещи сломались в вашем случае
Suvitruf
Там кое-какие щейдера поломались.
Но, что самое неприятное, странные вещи со скейлом произошли. Мы в этом месяце запустились на Facebook Gameroom, и вот там, в случае, когда игрок скейлит сам клиент перед запуском игры, нормально скейл не отрабатывает, координаты тачей ломаются, и перестаёт клавиатура работать. Плюс странная пикселизация наблюдается.