В цикле статей «Особенности разработки мобильной MMO RTS» мы расскажем о работе большой команды над масштабным проектом Stormfall: Rise of Balur. Этот опыт будет полезен независимым разработчиками и студиям, которые еще не определились с выбором технологий, архитектуры и структуры команды для своей RTS.



Выбор Unity. Преимущества и недостатки


Основным аргументом в пользу Unity был C#. Но есть и другие плюсы:

  • В Unity достаточно низкий порог входа для разработчиков.
  • Unity берет на себя все вопросы, связанные с совместимостью и корректной работой приложения на разных платформах. Нам только нужно убедиться в работоспособности билда в Unity Editor, проверить его на ключевых девайсах, а затем контролировать работу приложения через службы краш-репортов и наших Community Managers. Мы оповещаем о проблемах ребят из Unity, они это фиксят регулярными патч-релизами, а мы после этого выпускаем хот-фикс.
  • Unity обеспечивают высокий уровень поддержки разработчиков. Стандартные ответы от агентов – редкость. Если вопрос содержит подробное описание проблемы, скриншоты и тестовый проект для запуска, вы получите исчерпывающий ответ с советами по выходу из ситуации.

Теперь о плохом:

  • Низкий порог входа имел свои недостатки. Соискатели, которых мы собеседовали, неплохо знали Unity, но плохо – C#. Мы пришли к выводу, что лучше искать разработчиков с глубоким пониманием C# и желанием изучить самый популярный игровой движок.
  • Нестабильное качество релизов. Особенно это касается патчей. Unity советует ставить их только тогда, когда они исправляют баг, затрагивающий ваш проект. Но что-то может сломаться в другом месте.
  • Странные приоритеты в Roadmap. Например, долгие исследования полиморфной сериализации или вложенных префабов. Я думаю, что Unity пытается догнать своих ближайших конкурентов по качеству графики и не реализует фичи, которые очень сильно упростили бы разработку крупных проектов.
  • Закрытая платформа. При возникновении проблем, решение которых зависит от Unity, у вас нет других вариантов, кроме как ждать нужного релиза.

Архитектура


Если вы хотите прийти на рынок надолго, нужно хорошо подумать над архитектурой приложения. Если вы всё продумаете, сможете быстро и легко добавлять новые фичи и изменять старые. В сети есть куча статей по этой теме, но я хочу обратить внимание на несколько важных моментов:

  1. Выберите шаблон архитектуры на начальном этапе разработки и всегда придерживайтесь его. Для Unity-проектов это нужно, чтобы понять, как именно будет происходить обмен данными между UI и бизнес-логикой. Ваша задача сделать обмен однотипным, понятным и прозрачным.
  2. Не пытайтесь описать архитектуру целиком до начала проекта. Она начинает вырисовываться только на стадии разработки. Для старта достаточно следовать принципам выбранного шаблона и постепенно оформлять похожие механизмы в виде архитектурных решений. Но всё же на начальном этапе разработки архитектуре стоит уделять больше внимания, чем созданию новой функциональности.
  3. Закладывайте в сроки время на проведения рефакторинга для внедрения новых архитектурных решений.
  4. Создавайте ограничения на работу в коде, просто обсуждений недостаточно. Вы договариваетесь с командой, что какой-то объект можно использовать только определенным образом, из какого-то потока или для специфических условий. Проходит пару недель, и кто-то обязательно принимается использовать этот объект не там где нужно и не тем способом, о котором договаривались, что зачастую приводит к сложным для определения проблемам.
  5. Придерживайтесь принципов SOLID, но без фанатизма. Здравый смысл никто не отменял. Представьте, что у вас есть 2 пути. Первый – реализовать продуманное модульное техническое решение, которое легко расширяется в любом месте. Второй – выполнить бизнес-задачи в ограниченные сроки, но тогда вся красота технического решения «по барабану». В этом случае выбирайте второй путь. Не опускайтесь до разработки ради разработки.
  6. Принимайте важные решения вместе с командой. Попытайтесь для обсуждения предоставлять несколько вариантов с плюсами и минусами каждого из подходов.

Почему 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.

Реализованный у нас механизм биндингов работает так:

  1. На GameObject вешается UI-скрипт Label.
  2. На тот же GameObject вешается скрипт LabelBinding, он параметром принимает ссылку на Label, c которой и работает. Далее указывается путь к Property в ViewModel в строковом виде, с которой GameObject должен связаться.
  3. При Awake биндинг через ContextBox ищет в Context нужную ему Property по пути и, если находит, подписывается на её изменения.
  4. При изменении значения Property во ViewModel UI тут же реагирует на эти изменения и отображает их в ассоциированной Label.




Пока всё. Во второй части поговорим о многопоточности, работе со скинами, выполнении запросов и их кэшировании.
Поделиться с друзьями
-->

Комментарии (24)


  1. Suvitruf
    20.12.2016 10:12
    +1

    Соискатели, которых мы собеседовали, неплохо знали Unity, но плохо – C#.
    «Неплохо знали Unity» означает, что умеют в визуальный редактор?
    Закрытая платформа. При возникновении проблем, решение которых зависит от Unity, у вас нет других вариантов, кроме как ждать нужного релиза.
    Не совсем правда. У них можно попросить внутренний билд, если не боитесь. Плюс, у них разработчики некоторые идут на контакт с радостью, так что проблему можно прям с ними обсудить.


    1. Plarium
      20.12.2016 12:58

      «Неплохо знали Unity» означает, что умеют в визуальный редактор?

      Не только.
      Не совсем правда. У них можно попросить внутренний билд, если не боитесь. Плюс, у них разработчики некоторые идут на контакт с радостью, так что проблему можно прям с ними обсудить.

      Как было сказано выше в статье, у нас с Unity партнерские отношения, мы работаем с техподдержкой уровня Enterprise. Однако, с внутренним билдом часто возникают технические сложности, поэтому нам проще подождать релиза.


      1. Suvitruf
        21.12.2016 10:58

        Вот и мы ждали-ждали, вышла 5.5.0, а там кое-какие вещи сломались. Заметили это уже после апдейта в сторах =/


        1. KumoKairo
          21.12.2016 11:13

          Напишите пожалуйста какие конкретно вещи сломались в вашем случае


          1. Suvitruf
            21.12.2016 11:50

            Там кое-какие щейдера поломались.

            Но, что самое неприятное, странные вещи со скейлом произошли. Мы в этом месяце запустились на Facebook Gameroom, и вот там, в случае, когда игрок скейлит сам клиент перед запуском игры, нормально скейл не отрабатывает, координаты тачей ломаются, и перестаёт клавиатура работать. Плюс странная пикселизация наблюдается.


  1. Visteras
    20.12.2016 11:22

    На самом деле интересно, и надеюсь что вторая часть действительно будет.
    Плюс — интересно какой ЯП использовали для сервера?


    1. Plarium
      20.12.2016 12:20
      +1

      На сервере используется С#, соответственно у нас есть возможность частично шарить код между сервером и клиентом.


      1. Suvitruf
        20.12.2016 12:38

        Оу. У нас в игре тоже сервер на Юнити, что позволяет очень много кода шарить с клиентом. А можете поделиться информацией о перформансе сервера?

        Плюс, что более интересно, как управляете серверными инстансами? И т.п.


        1. Plarium
          20.12.2016 13:54

          Вы неправильно поняли: сервер у нас как раз не на Unity. Информацию по серверу можно прочитать в наших материалах от Павла Матлашова тут и тут.


  1. Folond
    20.12.2016 11:22

    Должен заметить такого рода статьи от Плариума радуют, очень мало в сети инфы о работе крупных проектов.


    1. Plarium
      20.12.2016 14:50

      Спасибо! Следите за обновлениями, впереди еще как минимум 5 статей о разработке этого проекта.


  1. GLeBaTi
    20.12.2016 11:22

    Если не затруднит, можно примеры кода?


    1. Plarium
      20.12.2016 12:21

      Примеры какого именно кода интересуют?


      1. GLeBaTi
        20.12.2016 13:12

        Пример вот этого:

        Далее указывается путь к Property в ViewModel в строковом виде, с которой GameObject должен связаться.
        При Awake биндинг через ContextBox ищет в Context нужную ему Property по пути и, если находит, подписывается на её изменения.
        При изменении значения Property во ViewModel UI тут же реагирует на эти изменения и отображает их в ассоциированной Label.


        1. Plarium
          20.12.2016 14:51

          Сейчас поищем.


        1. 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, путь задаётся так как на картинке ниже, в соответствии с именем переменной.



          1. Leopotam
            21.12.2016 00:01
            +1

            Те в случае вычисляемых полей при множественных зависимостях и при последовательном изменении значений этих зависимостей в одном обработчике у вас лейбл будет перезаписан несколько раз с соответствующими gc memory allocation на строках?


            1. Plarium
              21.12.2016 17:02

              Хорошее замечание, если мы вас правильно поняли. Чтоб этого избежать, мы собираем все изменения и выполняем только самое последнее в конце кадра.


              1. Leopotam
                21.12.2016 17:56

                А как производится вычисление составного поля? Например «прогресс 10 / 100», когда требуется несколько зависимых полей. Это решает дата-провайдер, получается, какую строку в какой виджет отдавать?


  1. OlegGelezcov
    20.12.2016 13:12

    Вы взаимойдествуете с сервером по протоколу http? Никаких сокет-соединений? Что ж это за реалтайм такой, опишите подробней?


    1. Suvitruf
      20.12.2016 13:46

      http с keep-alive, по сути, тоже самое, что и raw socket. Только лишних заголовков много. Если сообщения отправляются только в одну сторону, то вполне жизнеспособно.


      1. Leopotam
        20.12.2016 23:58

        WWW в юнити (а в статье указан именно он) раньше принудительно вырезал keep-alive и еще кучу заголовков, как сейчас — без понятия, но не думаю, что поведение сильно изменилось. Возможно новый вебреквест из HLAPI уже умеет такое.


        1. Suvitruf
          21.12.2016 00:25

          Мы для запросов к API используем UnityWebRequest с keep-alive. Работает отлично. Основное соединение с сервером через LLAPI. С WWW и других проблем много было. Благо сейчас есть хорошая замена.


    1. Plarium
      20.12.2016 14:45

      Да, на https с long polling для некоторых запросов. Одна из следующих статей в этом цикле будет посвящена как раз работе с сервером, там будет все расписано подробнее, так что следите за обновлениями :)