Особенности разработки мобильной MMO RTS. Часть 1

    В цикле статей «Особенности разработки мобильной 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.




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

    Другие статьи из серии:


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

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

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

        Как было сказано выше в статье, у нас с Unity партнерские отношения, мы работаем с техподдержкой уровня Enterprise. Однако, с внутренним билдом часто возникают технические сложности, поэтому нам проще подождать релиза.
        • 0
          Вот и мы ждали-ждали, вышла 5.5.0, а там кое-какие вещи сломались. Заметили это уже после апдейта в сторах =/
          • 0
            Напишите пожалуйста какие конкретно вещи сломались в вашем случае
            • 0
              Там кое-какие щейдера поломались.

              Но, что самое неприятное, странные вещи со скейлом произошли. Мы в этом месяце запустились на Facebook Gameroom, и вот там, в случае, когда игрок скейлит сам клиент перед запуском игры, нормально скейл не отрабатывает, координаты тачей ломаются, и перестаёт клавиатура работать. Плюс странная пикселизация наблюдается.
      • 0
        На самом деле интересно, и надеюсь что вторая часть действительно будет.
        Плюс — интересно какой ЯП использовали для сервера?
        • +1
          На сервере используется С#, соответственно у нас есть возможность частично шарить код между сервером и клиентом.
          • 0
            Оу. У нас в игре тоже сервер на Юнити, что позволяет очень много кода шарить с клиентом. А можете поделиться информацией о перформансе сервера?

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

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

          • 0
            Спасибо! Следите за обновлениями, впереди еще как минимум 5 статей о разработке этого проекта.
          • 0
            Если не затруднит, можно примеры кода?
            • 0
              Примеры какого именно кода интересуют?
              • 0
                Пример вот этого:
                Далее указывается путь к Property в ViewModel в строковом виде, с которой GameObject должен связаться.
                При Awake биндинг через ContextBox ищет в Context нужную ему Property по пути и, если находит, подписывается на её изменения.
                При изменении значения Property во ViewModel UI тут же реагирует на эти изменения и отображает их в ассоциированной Label.
                • 0
                  Сейчас поищем.
                  • 0
                    Если вас интересует реализация этого механизма, то показать мы его, к сожалению, не можем. Как и говорится в статье, для поиска необходимых 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
                      Те в случае вычисляемых полей при множественных зависимостях и при последовательном изменении значений этих зависимостей в одном обработчике у вас лейбл будет перезаписан несколько раз с соответствующими gc memory allocation на строках?
                      • 0
                        Хорошее замечание, если мы вас правильно поняли. Чтоб этого избежать, мы собираем все изменения и выполняем только самое последнее в конце кадра.
                        • 0
                          А как производится вычисление составного поля? Например «прогресс 10 / 100», когда требуется несколько зависимых полей. Это решает дата-провайдер, получается, какую строку в какой виджет отдавать?
                • 0
                  Я как-то использовал для NGUI штуку под названием NData. Где-то видел и под новый ui.
                • 0
                  Вы взаимойдествуете с сервером по протоколу http? Никаких сокет-соединений? Что ж это за реалтайм такой, опишите подробней?
                  • 0
                    http с keep-alive, по сути, тоже самое, что и raw socket. Только лишних заголовков много. Если сообщения отправляются только в одну сторону, то вполне жизнеспособно.
                    • 0
                      WWW в юнити (а в статье указан именно он) раньше принудительно вырезал keep-alive и еще кучу заголовков, как сейчас — без понятия, но не думаю, что поведение сильно изменилось. Возможно новый вебреквест из HLAPI уже умеет такое.
                      • 0
                        Мы для запросов к API используем UnityWebRequest с keep-alive. Работает отлично. Основное соединение с сервером через LLAPI. С WWW и других проблем много было. Благо сейчас есть хорошая замена.
                        • 0
                          Вы видимо не доконца прочитали, в статье написано так:

                          Все команды являются обертками над HttpWebRequest и выполняются асинхронно (Asynchronous Programming Model). Для WebGL-билда команды являются обертками над Unity WWW классом, который выполняется через корутины. Для коммуникации с сервером данные сериализуются в JSON-формат.
                          • 0
                            Так это не я не до конца прочитал, видимо. Еще раз тезисы: WebGL, WWW, нет поддержки Keep-Alive. Речь не про UnityWebRequest из новой сети, а про старый WWW — это 2 разных подсистемы.
                            • 0
                              Вы первую часть осознанно игнорируете?

                              Все команды являются обертками над HttpWebRequest


                              Stormfall: Rise of Balur — мобильная игра.
                              • 0
                                Эээ, еще раз прочитал — так вообще получается муть написана — javascript-обертки над браузерным HttpWebRequest нужны как раз только для WebGL (и будут работать только в нем). В чем смысл второй половины про WWW + коротины в WebGL?
                                • 0
                                  На сколько я понял HttpWebRequest для мобильной вресии игры, а WWW для WebGL.
                      • 0
                        Да, на https с long polling для некоторых запросов. Одна из следующих статей в этом цикле будет посвящена как раз работе с сервером, там будет все расписано подробнее, так что следите за обновлениями :)
                      • 0
                        Спасибо за цикл статей, очень интересно читать. Интернет наполнен туториалами для новичков, но вот таких действительно полезных статей, о том, как что устроено в реальном проекте — единицы. Есть вопрос про механизм биндингов: если в процессе рефакторинга кода решили переименовать у Tab поле Name в, допустим, Title, то как найти все GameObject-ы, которые подписаны на это поле, чтобы в строке-пути до Property тоже поменять на Title? Есть ли возможность делать это автоматически?
                        • 0
                          Возможность сделать это автоматически есть, но это нетривиальная задача. Мы заметили, что рефакторинги с переименованиями происходят нечасто, а если и происходят, то вместе с пересборкой UI, в котором заодно и правятся пути.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое