Привет! Совсем недавно я начал рассказывать о том, как мы работаем над Stormfall: Rise of Balur и пишем клиентскую часть проекта на Unity. Сегодня мы поговорим о подходе к скинованию, многопоточности, работе с сетью при плохом соединении и кэшировании запросов.
Жанр RTS хорошо адаптируется под разные сеттинги на базе одного движка. У нас есть несколько таких игр. При написании проекта меняется многое: UI, UX, логика геймплея. Иногда могут появляться специфические требования к интеграции библиотек и социальных сервисов. При проектировании игры мы должны были заложить требования в архитектуру, сохранив максимально возможный шаринг кода.
Чтобы кастомизировать поведение UI, мы решили сделать так, чтобы работа UI-объектов начиналась с динамической загрузки префабов из ресурсов. После этого выполняется специфический View-скрипт, который завязан на особые классы из ViewModel. Сама Модель написана так, чтобы поддерживать все фичи, потому что является отражением сервера и конфигурируется при логине с сервера.
В Unity отсутствует файл с описанием проекта: весь код, который находится в папке Assets, попадает в билд. При таких условиях создавать несколько проектов с общей код-базой сложно. Мы решили, что будем использовать общий репозиторий, в котором проект лежит не в виде Unity-проекта, а в том виде, который нам нужен.
С помощью скрипта, который создает симлинки на папку с кодом, мы разворачиваем Unity-проект. Если нужно, разворачиваем Dev-билд. В нем есть весь код, в том числе и из других проектов. Это нужно, например, для рефакторинга, а на геймплей никак не повлияет.
Когда мобильный рынок только набирал обороты, разработчики обсуждали, нужна ли многопоточность для их приложений. Сейчас это уже не вопрос для обсуждения. Многопоточностью мы решаем несколько задач:
Механизм доступа к данным модели работает через монитор и поддерживает различные политики блокирования данных. Он дает доступ на чтение множеству объектов одновременно, но не допускает возможность скомбинировать это с записью данных в Модель.
Unity не позволяет работать с движком из неосновного потока. Помните об этом при проектировании приложения и старайтесь разгрузить основной поток, вынося обработку не UI-данных в другие потоки.
В Stormfall: Rise of Balur взаимодействие с сервером происходит через HTTP-запросы. В случае ошибки запроса мы не всегда можем определить, на каком этапе произошла ошибка и был ли выполнен запрос на сервере. Нужно помнить, что мобильные игры работают через мобильный интернет, который не всегда стабилен. Чтобы обеспечить пользователям комфортный игровой процесс, мы реализовали несколько подходов:
Теперь более детально о каждом подходе.
Мы реализовали механизм в командах-запросах, который позволяет избежать блокировок UI при их выполнении. После того, как запрос начинает выполнятся, мы изменяем модель так, чтобы сервер как бы вернул успешный ответ. Если ответ был действительно успешным, мы ничего не меняем или дополняем модель ответом от сервера. Если сервер возвращает ошибку, вызываем метод отката изменений, которые выполнились на начальном этапе, и уведомляем об ошибке пользователя.
Что конкретно мы для этого сделали:
Перейдем к реализации второго подхода:
По статистике 0.76 % процентов запросов забираются из кэша, а это каждый 130-й запрос пользователя.
До встречи в третьей части! Если вы пропустили начало цикла о создании MMO RTS на Unity, ищите его здесь.
Скины и работа с ними
Жанр RTS хорошо адаптируется под разные сеттинги на базе одного движка. У нас есть несколько таких игр. При написании проекта меняется многое: UI, UX, логика геймплея. Иногда могут появляться специфические требования к интеграции библиотек и социальных сервисов. При проектировании игры мы должны были заложить требования в архитектуру, сохранив максимально возможный шаринг кода.
Чтобы кастомизировать поведение UI, мы решили сделать так, чтобы работа UI-объектов начиналась с динамической загрузки префабов из ресурсов. После этого выполняется специфический View-скрипт, который завязан на особые классы из ViewModel. Сама Модель написана так, чтобы поддерживать все фичи, потому что является отражением сервера и конфигурируется при логине с сервера.
В Unity отсутствует файл с описанием проекта: весь код, который находится в папке Assets, попадает в билд. При таких условиях создавать несколько проектов с общей код-базой сложно. Мы решили, что будем использовать общий репозиторий, в котором проект лежит не в виде Unity-проекта, а в том виде, который нам нужен.
С помощью скрипта, который создает симлинки на папку с кодом, мы разворачиваем Unity-проект. Если нужно, разворачиваем Dev-билд. В нем есть весь код, в том числе и из других проектов. Это нужно, например, для рефакторинга, а на геймплей никак не повлияет.
Многопоточность в Stormfall: Rise of Balur
Когда мобильный рынок только набирал обороты, разработчики обсуждали, нужна ли многопоточность для их приложений. Сейчас это уже не вопрос для обсуждения. Многопоточностью мы решаем несколько задач:
- Выполнение колбеков запросов на сервер в потоках из ThreadPool. Сами команды, помимо выполнения WebRequest, сериализируют отправляемые данные в формат JSON и десериализуют ответ. Еще они выполняют процесс обновления модели новыми данными, которые приходят в ответе.
- Выполнение логики механизма актуализации модели в отдельном потоке. Механизм срабатывает раз в секунду и может выполняться довольно долго.
Механизм доступа к данным модели работает через монитор и поддерживает различные политики блокирования данных. Он дает доступ на чтение множеству объектов одновременно, но не допускает возможность скомбинировать это с записью данных в Модель.
Unity не позволяет работать с движком из неосновного потока. Помните об этом при проектировании приложения и старайтесь разгрузить основной поток, вынося обработку не UI-данных в другие потоки.
Работа с сетью при плохом соединении
В Stormfall: Rise of Balur взаимодействие с сервером происходит через HTTP-запросы. В случае ошибки запроса мы не всегда можем определить, на каком этапе произошла ошибка и был ли выполнен запрос на сервере. Нужно помнить, что мобильные игры работают через мобильный интернет, который не всегда стабилен. Чтобы обеспечить пользователям комфортный игровой процесс, мы реализовали несколько подходов:
- Оптимистическое выполнение запросов.
- Перевыполнение запроса пользователя. Этот метод защищает от двойного выполнения команд, если запрос был на изменении данных и оборвался на возврате клиенту.
Теперь более детально о каждом подходе.
Оптимистичное выполнение запросов
Мы реализовали механизм в командах-запросах, который позволяет избежать блокировок UI при их выполнении. После того, как запрос начинает выполнятся, мы изменяем модель так, чтобы сервер как бы вернул успешный ответ. Если ответ был действительно успешным, мы ничего не меняем или дополняем модель ответом от сервера. Если сервер возвращает ошибку, вызываем метод отката изменений, которые выполнились на начальном этапе, и уведомляем об ошибке пользователя.
Что конкретно мы для этого сделали:
- Реализовали отдельный вид команд, который описывает возможность предварительного выполнения запроса, не дожидаясь ответа сервера.
- Сделали так, что запросы выполняются строго последовательно с помощью очереди. В ней могут находиться 2 запроса: активный и ожидающий. Их так мало, потому что мы не хотим, чтобы пользователь мог запланировать много действий. Если первая команда возвращает ошибку, то все остальные команды нужно отменять, иначе их выполнение приведет к неконсистентности данных. Не исключаем, что пользователь может выйти из игры, и очередь команд так и не будет отправлена на сервер для выполнения. Если очередь заполнена, пользователю показывается окно ожидания.
- Добились того, что в случае ошибки выполнения запроса для него откатываются все произведенные действия, а следующий запрос в очереди, если он присутствует, отменяется. Откат действий программируется вручную.
Кэширование редактирующих запросов на стороне сервера
Перейдем к реализации второго подхода:
- Если клиент получает ошибку сети при запросе на сервер, то пытается заново отправить запрос.
- Каждому запросу выделяется свой идентификатор.
- В случае перевыполнения запроса в заголовок также записывается номер попытки.
- Для каждого последующего запроса таймаут увеличивается с 10 секунд до 20. Мы сделали это для случаев, когда у пользователя плохой интернет и не хватает скорости за отведенное время загрузить большой ответ от сервера. Может показаться, что в этом нет смысла, можно же сразу поставить максимальное значение. На практике оказывается, что запрос, который отпал по сетевым причинам, повторится с минимальным интервалом. Это лучше ожидания максимального таймаута и повтора запроса.
- Если все запросы завершились неудачей, мы показываем пользователю информацию об ошибке, а при достижении максимального количества сетевых ошибок считаем, что сессию невозможно продолжать, и предлагаем пользователю перезайти в игру.
- На стороне сервера ненадолго кэшируются несколько последних запросов редактирования для каждого пользователя. При получении запроса с идентификатором, который уже был выполнен, возвращается кэшированный результат – конечно, если он есть. Если его нет, сервер возвращает ошибку.
- Запросы на чтение не кэшируются и всегда обрабатываются сервером по новой.
По статистике 0.76 % процентов запросов забираются из кэша, а это каждый 130-й запрос пользователя.
До встречи в третьей части! Если вы пропустили начало цикла о создании MMO RTS на Unity, ищите его здесь.