Привет! Совсем недавно я начал рассказывать о том, как мы работаем над Stormfall: Rise of Balur и пишем клиентскую часть проекта на Unity. Сегодня мы поговорим о подходе к скинованию, многопоточности, работе с сетью при плохом соединении и кэшировании запросов.



Скины и работа с ними


Жанр RTS хорошо адаптируется под разные сеттинги на базе одного движка. У нас есть несколько таких игр. При написании проекта меняется многое: UI, UX, логика геймплея. Иногда могут появляться специфические требования к интеграции библиотек и социальных сервисов. При проектировании игры мы должны были заложить требования в архитектуру, сохранив максимально возможный шаринг кода.

Чтобы кастомизировать поведение UI, мы решили сделать так, чтобы работа UI-объектов начиналась с динамической загрузки префабов из ресурсов. После этого выполняется специфический View-скрипт, который завязан на особые классы из ViewModel. Сама Модель написана так, чтобы поддерживать все фичи, потому что является отражением сервера и конфигурируется при логине с сервера.



В Unity отсутствует файл с описанием проекта: весь код, который находится в папке Assets, попадает в билд. При таких условиях создавать несколько проектов с общей код-базой сложно. Мы решили, что будем использовать общий репозиторий, в котором проект лежит не в виде Unity-проекта, а в том виде, который нам нужен.

С помощью скрипта, который создает симлинки на папку с кодом, мы разворачиваем Unity-проект. Если нужно, разворачиваем Dev-билд. В нем есть весь код, в том числе и из других проектов. Это нужно, например, для рефакторинга, а на геймплей никак не повлияет.

Многопоточность в Stormfall: Rise of Balur


Когда мобильный рынок только набирал обороты, разработчики обсуждали, нужна ли многопоточность для их приложений. Сейчас это уже не вопрос для обсуждения. Многопоточностью мы решаем несколько задач:

  1. Выполнение колбеков запросов на сервер в потоках из ThreadPool. Сами команды, помимо выполнения WebRequest, сериализируют отправляемые данные в формат JSON и десериализуют ответ. Еще они выполняют процесс обновления модели новыми данными, которые приходят в ответе.
  2. Выполнение логики механизма актуализации модели в отдельном потоке. Механизм срабатывает раз в секунду и может выполняться довольно долго.

Механизм доступа к данным модели работает через монитор и поддерживает различные политики блокирования данных. Он дает доступ на чтение множеству объектов одновременно, но не допускает возможность скомбинировать это с записью данных в Модель.

Unity не позволяет работать с движком из неосновного потока. Помните об этом при проектировании приложения и старайтесь разгрузить основной поток, вынося обработку не UI-данных в другие потоки.



Работа с сетью при плохом соединении


В Stormfall: Rise of Balur взаимодействие с сервером происходит через HTTP-запросы. В случае ошибки запроса мы не всегда можем определить, на каком этапе произошла ошибка и был ли выполнен запрос на сервере. Нужно помнить, что мобильные игры работают через мобильный интернет, который не всегда стабилен. Чтобы обеспечить пользователям комфортный игровой процесс, мы реализовали несколько подходов:

  1. Оптимистическое выполнение запросов.
  2. Перевыполнение запроса пользователя. Этот метод защищает от двойного выполнения команд, если запрос был на изменении данных и оборвался на возврате клиенту.

Теперь более детально о каждом подходе.

Оптимистичное выполнение запросов


Мы реализовали механизм в командах-запросах, который позволяет избежать блокировок UI при их выполнении. После того, как запрос начинает выполнятся, мы изменяем модель так, чтобы сервер как бы вернул успешный ответ. Если ответ был действительно успешным, мы ничего не меняем или дополняем модель ответом от сервера. Если сервер возвращает ошибку, вызываем метод отката изменений, которые выполнились на начальном этапе, и уведомляем об ошибке пользователя.

Что конкретно мы для этого сделали:

  1. Реализовали отдельный вид команд, который описывает возможность предварительного выполнения запроса, не дожидаясь ответа сервера.
  2. Сделали так, что запросы выполняются строго последовательно с помощью очереди. В ней могут находиться 2 запроса: активный и ожидающий. Их так мало, потому что мы не хотим, чтобы пользователь мог запланировать много действий. Если первая команда возвращает ошибку, то все остальные команды нужно отменять, иначе их выполнение приведет к неконсистентности данных. Не исключаем, что пользователь может выйти из игры, и очередь команд так и не будет отправлена на сервер для выполнения. Если очередь заполнена, пользователю показывается окно ожидания.
  3. Добились того, что в случае ошибки выполнения запроса для него откатываются все произведенные действия, а следующий запрос в очереди, если он присутствует, отменяется. Откат действий программируется вручную.

Кэширование редактирующих запросов на стороне сервера


Перейдем к реализации второго подхода:

  1. Если клиент получает ошибку сети при запросе на сервер, то пытается заново отправить запрос.
  2. Каждому запросу выделяется свой идентификатор.
  3. В случае перевыполнения запроса в заголовок также записывается номер попытки.
  4. Для каждого последующего запроса таймаут увеличивается с 10 секунд до 20. Мы сделали это для случаев, когда у пользователя плохой интернет и не хватает скорости за отведенное время загрузить большой ответ от сервера. Может показаться, что в этом нет смысла, можно же сразу поставить максимальное значение. На практике оказывается, что запрос, который отпал по сетевым причинам, повторится с минимальным интервалом. Это лучше ожидания максимального таймаута и повтора запроса.
  5. Если все запросы завершились неудачей, мы показываем пользователю информацию об ошибке, а при достижении максимального количества сетевых ошибок считаем, что сессию невозможно продолжать, и предлагаем пользователю перезайти в игру.
  6. На стороне сервера ненадолго кэшируются несколько последних запросов редактирования для каждого пользователя. При получении запроса с идентификатором, который уже был выполнен, возвращается кэшированный результат – конечно, если он есть. Если его нет, сервер возвращает ошибку.
  7. Запросы на чтение не кэшируются и всегда обрабатываются сервером по новой.

По статистике 0.76 % процентов запросов забираются из кэша, а это каждый 130-й запрос пользователя.

До встречи в третьей части! Если вы пропустили начало цикла о создании MMO RTS на Unity, ищите его здесь.
Поделиться с друзьями
-->

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


  1. Suvitruf
    04.01.2017 15:08

    Добились того, что в случае ошибки выполнения запроса для него откатываются все произведенные действия, а следующий запрос в очереди, если он присутствует, отменяется. Откат действий программируется вручную.
    То есть, если в очереди 2 совершенно несвязанных запроса, то при ошибке в одном из них отменяется и другой? Зачем? К примеру, 1 запрос на строительство чего-то, а второй на скликивание ресурсов/выполнение задания. Пользователю придётся заново второе действие повторять? А если это действие выполняется где-то во внутреннем окне, то это может быть сразу несколько лишних кликов.


    1. Plarium
      04.01.2017 19:19

      Резонное замечание. Суть в том, что с вероятностью 90% запросы, которые выполняются по такой схеме, зависят друг от друга, т.к. используют различного вида ресурсы. Так же помимо ресурсов есть и другие виды логических зависимостей. В вашем примере всё выглядит логично и нет никакого смысла отменять второй запрос. Но на деле нужно либо руками помечать такие команды, либо писать механизм который будет это как-то эвристически определять.


  1. vagran
    04.01.2017 22:00

    В Unity отсутствует файл с описанием проекта: весь код, который находится в папке Assets, попадает в билд. При таких условиях создавать несколько проектов с общей код-базой сложно. Мы решили, что будем использовать общий репозиторий, в котором проект лежит не в виде Unity-проекта, а в том виде, который нам нужен. С помощью скрипта, который создает симлинки на папку с кодом, мы разворачиваем Unity-проект.

    Что-то странное. Можно ведь компилировать сборки вообще отдельно от Unity, и потом подкладывать в проект. В нашем проекте, к примеру, вся кодовая база в виде отдельного солюшена и компилируется в VS в кучу сборок. А в проекте Юнити только пустой объект со ссылкой на стартовый behavior в одной из таких сторонних сборок. И всё прекрасно шарится, делаются нормальные общие библиотечные сборки для разных компонентов. Никаких махинаций с проектом юнити и симлинками.


    1. WeslomPo
      04.01.2017 23:12

      В этой цитате показывается неосведомленность о том, как Unity собирает проект на самом деле. Если коротко, то из папки Assets в проект попадает только то, что используется на сцене, которая попадает в проект (в настройках билда), либо через префабы которые используются на сцене попадающей в проект — т.е если есть прямая ссылка на asset со сцены тем или иным образом.

      100% в билд попадает только содержимое папки Resources и папки Streaming Assets. В первом случае к ресурсам можно обратится через Resource, во втором через File.

      Простой способ увеличить скорость компилирования проекта, засунуть библиотечный (не ссылающийся на код игры) код в папку Plugins. Вообще я видел проект на гитхабе который с помощью симлинков тюнил скорость компиляции и гибкий шаринг скриптов между проектами.


      1. vagran
        10.01.2017 21:55

        У нас все DLLки, собранные VS, кладутся в Assets/Lib. Редактор там их видит (позволяет навешивать behavior'ы из них на объекты сцены, и всё работает при запуске из редактора). Насчёт попаданию в сборку не помню, у нас после сборки Юнити отрабатывают ещё дополнительные скрипты, которые формируют окончательное содержимое пакета, для каждой из платформ. Там и эти библиотеки могут куда-то копироваться, и дополнительные сторонние зависимости, и формируется конфигурационный файл mono, в котором прописывается, где искать различные нативные системные библиотеки на каждой платформе, типа libpng или sqlite).


    1. WeslomPo
      04.01.2017 23:20

      Кстати, было бы интересно почитать статью об организации подобного проекта, как у вас в комментарии.
      Я сейчас занимаюсь разработкой чего-то подобного, и, похоже, собираю грабли ;( от чего проект движется как-то медленно.


      1. vagran
        10.01.2017 22:11

        Статью, наверно, не осилю (главным образом, потому что в основном занимаюсь разработкой других компонентов на Линуксе, и на Windows и VS переключаюсь с большой неохотой), но здесь в комментариях могу проконсультировать по возникшим вопросам. Почитайте несколько моих новых комментариев, я там в общих чертах описал принципы.


    1. Temka193
      10.01.2017 18:41

      Что значит «стартовый behavior»? То есть на сцене висит GameObject, на котором MonoBehaviour из Dll?
      Это используется в качестве точки входа?


      1. vagran
        10.01.2017 21:47

        Да, пустой объект, к которому подцеплен Behaviour из DLL проекта, собранного в VS. Юнити видит все эти дллки и behavor'ы в них, и позволяет выбрать нужный. Связь сохраняется по GUID дллки в meta-файлов проекта юнити.


        1. vagran
          10.01.2017 22:04

          Ну и дополню, что этот пустой объект единственное содержимое сцены, всё создаётся из кода при старте. В проекте Юнити только ресурсы (картинки, меши). Чтобы можно было использовать VS, нужно в солюшене сделать ссылку на UnityEngine.dll из поставки Unity. Также для VS есть свободный плагин, позволяющий подключаться отладчиком к редактору или плееру (так же, как раньше было в mono develop в комплекте с Юнити). В общем, процесс разработки кода достаточно удобно организуется при таком подходе.