Всем привет, сегодня мы расскажем о том, как мы делим качества и какие инструменты для этого используем в проекте War Robots.
Релиз War Robots состоялся еще в 2014 году, и за 7 лет существования проекта графическая часть в нем постоянно развивалась. Но в то же время команда постоянно сталкивалась с ограничениями из-за минимальных требований к девайсам. Оперируя таким большим проектом, у которого немало устройств входит в low-end сегмент, нельзя просто взять и запилить крутой современный графен, не потеряв при этом часть аудитории.
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/82d/5c8/4ea/82d5c84eab327d6c284899ace12cdc3a.png)
Так у нас появилась задача: сделать всем красиво и хорошо. Поэтому мы решили делать War Robots Remastered — с блэк-джеком, обновлением графического пайплайна и разделением ассетов на разные качества.
Первое и самое очевидное, что от нас требовалось, — для разных групп девайсов сделать контент, удовлетворяющий требованиям по картинке и производительности.
На тот момент билд War Robots под Android со всеми ресурсам весил порядка 700 МБ и включал сотни единиц контента. У нас было 13 карт, 81 мех, более ста пушек, десяток дронов и еще куча всякой мелочи. Не то, чтобы все это было категорически необходимо в проекте, но если уж начал пилить контент, то иди в своем увлечении до конца.
И второе — нам нужно этими качествами как-то управлять и предоставлять пользователю то качество, которое будет оптимально для его девайса.
У нас уже был менеджер качества, состоящий из динамических пресетов и представляющий собой ScriptableObject с кастомным InspectorGUI. Это была длиннющая портянка с настройками и пресетами, почти полностью завязанная на логику «Роботов», и каждое поле в настройке рисовалось кодом. Хочешь добавить параметр в настройку — не забудь отрисовать это в InspectorGUI. Выглядело это монструозно, так как из-за большого количества настроек число фолдаутов в инспекторе достигало более 9000.
Динамический скейлинг параметров игры давал свои преимущества, но у него также было и несколько минусов, из-за которых нам пришлось от него отказаться. В первую очередь — из-за количества пресетов. Со временем из-за разной архитектуры поддерживаемых устройств количество пресетов качества накапливалось, пока не достигло 15 штук. В реальности мало кто обращал на это внимание и, конечно, никто не тестировал все 15 пресетов на каждом устройстве в продакшене. К тому же, поддерживать такое количество пресетов довольно сложно и с точки зрения разработки: никогда не знаешь, когда старый Quality Manager возьмет и переключит параметры внутри текущего качества — и какие именно. Вдобавок, когда мы начали работу над новым кастомным рендер-движком и на основе него запрофилировали все 15 качеств, оказалось, что разброс по производительности между ними не превышает 15-20%.
Все это нас не устраивало, так что мы решили изменить подход к формированию пресетов и запилить новый Quality Manager. А задачу эту передали нашему отделу кросс-проектной разработки, именуемому Platform Team.
Теперь поговорим о том, что же такое новый Quality Manager в War Robots, какие задачи он решает и как устроен.
![](https://habrastorage.org/r/w1560/webt/uu/y2/il/uuy2il21ygdwppcvk4_w6r6anam.png)
Основная задача заключалась в том, чтобы создать инструмент, с помощью которого можно с минимальным временными затратами добавлять в проект настройки качества, формировать из них пресеты и задавать параметры, на основе которых пользователю будет выбран оптимальный для его устройства пресет.
Главными сущностями у нас являются:
Quality Manager состоит из двух частей: runtime-часть с API для инициализации и переключения качеств и editor-часть с GUI, которые позволяют все это конфигурировать.
Начнем с editor-части. Ее задача — предоставление интерфейса для конфигурации настроек качества и пресетов и минимизация количества кода, необходимого со стороны клиента. В идеале мы хотели заставить клиентщиков описывать только структуры настроек качества, а всю работу по отрисовке оставить на стороне Quality Manager.
Вот так выглядит наше окно, разделенное на три вкладки: настройки качества, пресеты, группы устройств:
![](https://habrastorage.org/r/w1560/webt/zl/wc/gx/zlwcgxtasx1smit4yk9mdxr_a-e.png)
Давайте подробнее разберемся, как с этим работать, и начнем с вкладки Quality Settings.
Так выглядят классы «базовых настроек» и «настроек кэша пререндеров» в War Robots. Эти же классы затем используются в рантайме:
А вот так это выглядит в окне Quality Manager:
![](https://habrastorage.org/r/w1560/webt/x-/tu/sp/x-tusprqgk5ilqrqtlf8mhaaapi.png)
![](https://habrastorage.org/r/w1560/webt/_g/42/iv/_g42ivwsgj474_wqkqd14qx0b4u.png)
Получается достаточно простая схема. Клиентские разработчики создают класс с набором полей и настраивают уровни качества для него в окне Quality Manager. Он, в свою очередь, «из коробки» умеет отрисовывать примитивные типы, Enum, Nullable, массивы, списки, интерфейсы и собственные классы c полями вышеперечисленных типов, включая другие классы. На случай, когда необходимо отрисовывать для поля кастомный GUI, в QM предусмотрена возможность помечать поля атрибутом, в классе которого реализована отрисовка этого поля.
Так, например, выглядит код класса, меняющего отрисовку для int значения c IntFiled на Slider с параметрами шага, минимального и максимального значений:
А так — поле с этим атрибутом:
В QM сразу включен ряд реализаций для кастомной отрисовки полей: IntSliderView, FloatSliderView, IntPopupView, PresetIndexView, PresetNameView, QualitySettingIndexView, QualitySettingNameView. Этого скромного набора нам хватило для интеграции QM в War Robots и переезда со старого QM на новый. Кода в проекте стало ощутимо меньше, а тот, который остался, стал заметно проще, понятнее и описывал именно то, что он и должен был описывать — данные и логику работы с ними.
У нас уже есть классы настроек качества и данные для их уровней, так что пора формировать из них пресеты. Для этого отправляемся на первую вкладку окна QM — Presets.
![](https://habrastorage.org/r/w1560/webt/xh/pf/px/xhpfpxhwksqcqy_trbobkfnyjju.png)
Тут все достаточно тривиально: мы заводим необходимое количество пресетов, задаем им имена и выставляем для них уровни настроек качества. Готово.
Теперь, когда у нас есть пресеты, осталось задать параметры, по которым будет определяться, какой пресет использовать на девайсе.
Третья вкладка — Device Groups.
![](https://habrastorage.org/r/w1560/webt/zz/av/xe/zzavxehlo6vngnfgwpff3f7oyyo.png)
На этой вкладке мы формируем группы девайсов. Для этого мы используем несколько параметров: объем ОП, частоту процессора, модель GPU и модель девайса для совсем точного попадания. Все параметры не являются обязательными, и можно для группы указать только часть из них. Так, например, для устройств на iOS самый простой вариант — составить карту по модели устройств. На Android же большую часть покроют группы, объеденные по популярным моделям GPU, а в остальных случаях можно указать минимальные требования по объему оперативной памяти и частоте процессора.
Для группы — помимо пресета, который будет выбран по умолчанию — мы также задаем список доступных этой группе устройств пресетов для того, чтобы пользователь на low-end девайсе не смог поменять настройки на ultra high, что может привести к крешам по OOM на старте приложения и блокировать тем самым возможность изменить настройки обратно.
Итак, конфиг QM готов. Сериализуется он в JSON, что дает возможность его легко читать и править без окна QM, а также доставлять на клиент с сервера.
Помимо описанного функционала, QM позволяет:
API рантайм-части достаточно простой и включает основные методы для работы с QM:
Так инициализируется инстанс QM:
Помимо основных методов, инстанс содержит ивенты о смене пресета и уровня настройки качества. Логика применения настроек качества лежит на клиентской стороне — для этого у класса QualitySetting есть виртуальный метод Apply, который вызывается при смене уровня настройки качества.
Помимо базовых методов API, в QM есть набор методов для «мягкого» переключения уровней качества. В случае, когда в рантайме мы сталкиваемся с ситуацией, при которой девайс должен тянуть заданный ему пресет, но при этом происходит падение FPS из-за посторонних факторов, можно ослабить нагрузку, понизив одну из настроек качества, или наоборот. Для этого в QM есть отдельный тип настроек качества, у которого есть виртуальный метод CanBeSwitchedTo, позволяющий определить, можно ли сменить текущий уровень настройки на новый. Так мы можем в рантайме поэтапно даунскейлить качество, чтобы стабилизировать FPS, или наоборот — попробовать дать девайсу нарисовать лучшую картинку, пока не начнем ловить падение FPS.
![](https://habrastorage.org/r/w1560/webt/vm/mf/pv/vmmfpvwwr_mfm3_lqsyzt8cnk3e.png)
Для этого API QM содержит набор методов для попытки даунгрейда или апгрейда уровня настроек качества. При этом можно задать как конкретный тип настройки качества, так и предоставить системе самой решать, какую из них менять. В этом случае система выберет настройку, приводящую к минимальному изменению общего качества, и будет выбирать настройки с большим количеством уровней качества.
При смене пресета/уровня настройки качества QM сохраняет стейт и при последующей инициализации использует уже его. По умолчанию данные сохраняются в PrefsManager, и сохраненный стейт используется до тех пор, пока не будет изменена версия конфига QM. При необходимости можно реализовать свой вариант IStateController и использовать эту реализацию при инициализации QM, чтобы определить, когда можно использовать стейт от старой версии конфига, а когда смена конфига должна приводить к сбросу пресета на дефолтный.
Конфиг для QM лежит в папке ресурсов и по умолчанию грузится из билда. Дополнительно к этому мы предусмотрели механизм удаленного обновления конфига с CDN. Для этого файл конфигурации выкладывается на CDN, а на клиент приходит ссылка на него с указанием хеша конфига. По хешу система определяет, нужно ли ей обновить конфиг перед инициализацией или на девайсе уже есть актуальная версия конфига. Так мы в любой момент при необходимости можем поменять настройки QM у всех пользователей.
С QM разобрались. Давайте теперь более предметно рассмотрим, как мы сформировали список настроек качества, пресетов и групп устройств на проекте War Robots.
Мы решили зафиксировать несколько четко установленных пресетов, а динамики достигать так же, как и в консольных играх, — за счет скейлинга картинки. Современные девайсы обладают экранами с высоким разрешением, однако GPU в них стоят, конечно, далеко не RTX 3090, так что было бы наивно полагать, что они будут справляться с 60 FPS в нативных Quad HD или даже 4k. Собственно, мы сразу ограничили плотность пикселей сверху, проитерировавшись до значения в 350 ppi.
Изначально при работе над ремастером мы фокусировались на двух качествах — HD (high definition) и LD (low definition). Весь контент, который мы переделывали, основывался на них, и все инструменты по автоматической генерации исходили тоже из них. Однако вскоре мы поняли, что нам понадобится дополнительный уровень качества, который будет нацелен на устройства с небольшим, по нашим меркам, объемом RAM. Так родился еще один пресет качества — ULD (ultra low definition).
Так выглядит игра в качестве HD:
![](https://habrastorage.org/r/w1560/webt/rc/-v/vx/rc-vvxerbeac5e875lxn_tfdmxm.png)
Так — в LD:
![](https://habrastorage.org/r/w1560/webt/an/3a/3q/an3a3qvrpodaorkay87lj7x-ors.png)
А так — в ULD:
![](https://habrastorage.org/r/w1560/webt/gi/ge/iz/gigeizwk8csdu1ihxxmbl339o6w.png)
Каждый пресет мы разграничили не только по качеству, но и по максимально возможному FPS. Сейчас мы позволяем выбирать некоторым устройствам 60 FPS, что достигается благодаря отдельной настройке внутри нового QM:
![](https://habrastorage.org/r/w1560/webt/nd/pt/z1/ndptz1kcw9ft5yu2mgeoxq0snha.png)
Как мы видим, каждый пресет имеет настройку TargetFPSQualitySettings и внутри нее два уровня, отвечающие за максимально возможный FPS на устройстве. Затем «глобальные» пресеты качеств также разделяются: например, есть качество LD, а есть LD60. Это значит, что пользователи, устройства которых попадают в группу LD60 (по названию устройства — на iOS или по GPU — на Android), получают возможность в настройках включить 60 FPS:
![](https://habrastorage.org/r/w1560/webt/0q/9f/lk/0q9flkdbjgbf5osmkqxztvyyqjg.png)
Какое-то время мы рассуждали, нужно ли включать пользователям 60 FPS по умолчанию, но пришли к тому, что не стоит: это значительно повысит использование батареи мобильного устройства, а по нашим внутренним данным на разных проектах на такую частоту кадров переключаются 5-15% аудитории (у которой эта настройка вообще доступна).
Также внутри QM содержится важнейший параметр — с каким «тэгом» ресурсной системы работать:
![](https://habrastorage.org/r/w1560/webt/iu/qe/v0/iuqev0oohq_8yggw98ssagtic1m.png)
Так, для ULD качества используется тег LD_ULD, который содержит набор ресурсов, упакованных нашей ресурсной системой для этих качеств. Объединение этих двух качеств дало нам большую экономию на дубликатах ресурсов, которые складываются в Asset Bundles — но это, я думаю, мы расскажем в наших следующих статьях.
Таким вот образом «собирается» каждое качество: это всего лишь набор уровней настроек.
Для того, чтобы применить настройки в рантайме, используется система наследования классов QM. Разберем пример применения настроек рендера:
![](https://habrastorage.org/r/w1560/webt/4_/eq/pw/4_eqpwhoptn8gvggffarcfzgldm.png)
Как пример, для ULD настройки рендера используется MasterTextureLimit = 1. Давайте посмотрим, как мы можем применить его к нашей игре.
Каждая настройка должна переопределять абстрактный класс WRQualitySetting, что и делает наш пользовательский класс RenderingQualitySetting:
Благодаря этому наш класс может перегружать метод Apply:
Собственно, при вызове этого метода мы можем делать что угодно. В этот момент мы знаем, что наши ресурсы загружены, а рендер-пайплайн уже готов к работе.
Вызов метода Apply происходит в двух случаях:
Вызов API QualityManager ApplicationContext.QualityService.QualityManager.SetCurrentPreset вызовет, в свою очередь, череду изменений внутри конфигураций и в конце переключит и применит все настройки, которые были зарегистрированы в QM.
Наверное, один из самых важных этапов, который был произведен перед переходом на новый QM — это чистка старых параметров качества.
За долгий срок разработки проекта скопилось приличное количество давно неиспользуемых полей или тех, которые имеют минимальное значение для производительности, а также пресетов качества, которые были созданы под устройства, которые мы давно не поддерживаем.
Мы смогли выделить основные необходимые для нас параметры, которыми хотелось управлять, а также разделили их на условные группы:
![](https://habrastorage.org/r/w1560/webt/eu/yl/tj/euyltju2-w1pyjhfjzvci804ej8.png)
Стоит отметить также, как раньше происходил выбор качества, которое нужно выставить на устройстве. Был большой CS-файл, в котором кодом описывались характеристики нужных устройств. На первый взгляд, такой подход может показаться наивным и не гибким, однако у него есть свои плюсы.
На практике довольно сложно законфигурировать все устройства идеально, и бывают ситуации, когда на разных устройствах с одинаковым, казалось бы, SoC, игра ведет себя совершенно по-разному. Могут быть некачественные детали (например, дешевая память с низкими характеристиками) или некорректно написанные драйверы. В этом случае девайс всегда можно выделить отдельно и подобрать настройки под него. При создании QM мы учли подобные случаи, и мы можем производить конфигурацию пресетов не только per-GPU но и per-device (это активно используется, например, на Apple-устройствах).
Отдельно стоит отметить, что у нас имеется возможность как хранить манифест QM внутри игры, так и на CDN, и доставлять его в клиент динамически на старте. Определение наиболее актуального происходит простым определением наличия ссылки на нашем мета сервере. Если она есть, то конфиг всегда берется с сервера. Также на мета-сервере имеется возможность разделять конфиги по версиям клиента, поскольку у нас бесшовные обновления, и несколько версий клиента живут в проде одновременно.
Новый Quality Manager дал нам довольно большие возможности: это и мощность управления конфигурациями из старой системы, и простота тестирования, и возможность менять параметры буквально на лету через сервер, и упрощение разработки графического пайплайна. Также QM удобным образом позволил нам разделить настройки качества и выдать хорошую графику на телефонах, которые ее поддерживают, при этом сохранив приближенную к старой на слабеньких устройствах в таком же FPS, как в оригинальной игре.
Авторы материала: Дмитрий Самсонов, Senior Platform Developer, Павел Зинов, Head of Client Department
Релиз War Robots состоялся еще в 2014 году, и за 7 лет существования проекта графическая часть в нем постоянно развивалась. Но в то же время команда постоянно сталкивалась с ограничениями из-за минимальных требований к девайсам. Оперируя таким большим проектом, у которого немало устройств входит в low-end сегмент, нельзя просто взять и запилить крутой современный графен, не потеряв при этом часть аудитории.
![image](https://habrastorage.org/getpro/habr/post_images/82d/5c8/4ea/82d5c84eab327d6c284899ace12cdc3a.png)
Так у нас появилась задача: сделать всем красиво и хорошо. Поэтому мы решили делать War Robots Remastered — с блэк-джеком, обновлением графического пайплайна и разделением ассетов на разные качества.
Первое и самое очевидное, что от нас требовалось, — для разных групп девайсов сделать контент, удовлетворяющий требованиям по картинке и производительности.
На тот момент билд War Robots под Android со всеми ресурсам весил порядка 700 МБ и включал сотни единиц контента. У нас было 13 карт, 81 мех, более ста пушек, десяток дронов и еще куча всякой мелочи. Не то, чтобы все это было категорически необходимо в проекте, но если уж начал пилить контент, то иди в своем увлечении до конца.
И второе — нам нужно этими качествами как-то управлять и предоставлять пользователю то качество, которое будет оптимально для его девайса.
У нас уже был менеджер качества, состоящий из динамических пресетов и представляющий собой ScriptableObject с кастомным InspectorGUI. Это была длиннющая портянка с настройками и пресетами, почти полностью завязанная на логику «Роботов», и каждое поле в настройке рисовалось кодом. Хочешь добавить параметр в настройку — не забудь отрисовать это в InspectorGUI. Выглядело это монструозно, так как из-за большого количества настроек число фолдаутов в инспекторе достигало более 9000.
Динамический скейлинг параметров игры давал свои преимущества, но у него также было и несколько минусов, из-за которых нам пришлось от него отказаться. В первую очередь — из-за количества пресетов. Со временем из-за разной архитектуры поддерживаемых устройств количество пресетов качества накапливалось, пока не достигло 15 штук. В реальности мало кто обращал на это внимание и, конечно, никто не тестировал все 15 пресетов на каждом устройстве в продакшене. К тому же, поддерживать такое количество пресетов довольно сложно и с точки зрения разработки: никогда не знаешь, когда старый Quality Manager возьмет и переключит параметры внутри текущего качества — и какие именно. Вдобавок, когда мы начали работу над новым кастомным рендер-движком и на основе него запрофилировали все 15 качеств, оказалось, что разброс по производительности между ними не превышает 15-20%.
Все это нас не устраивало, так что мы решили изменить подход к формированию пресетов и запилить новый Quality Manager. А задачу эту передали нашему отделу кросс-проектной разработки, именуемому Platform Team.
Теперь поговорим о том, что же такое новый Quality Manager в War Robots, какие задачи он решает и как устроен.
![](https://habrastorage.org/webt/uu/y2/il/uuy2il21ygdwppcvk4_w6r6anam.png)
Так что это за менеджер качества такой
Основная задача заключалась в том, чтобы создать инструмент, с помощью которого можно с минимальным временными затратами добавлять в проект настройки качества, формировать из них пресеты и задавать параметры, на основе которых пользователю будет выбран оптимальный для его устройства пресет.
Главными сущностями у нас являются:
- QualitySetting — набор параметров, объединенных в общую группу;
- Preset, состоящий из выбранных уровней QualitySetting разного типа.
Quality Manager состоит из двух частей: runtime-часть с API для инициализации и переключения качеств и editor-часть с GUI, которые позволяют все это конфигурировать.
Editor
Начнем с editor-части. Ее задача — предоставление интерфейса для конфигурации настроек качества и пресетов и минимизация количества кода, необходимого со стороны клиента. В идеале мы хотели заставить клиентщиков описывать только структуры настроек качества, а всю работу по отрисовке оставить на стороне Quality Manager.
Вот так выглядит наше окно, разделенное на три вкладки: настройки качества, пресеты, группы устройств:
![](https://habrastorage.org/webt/zl/wc/gx/zlwcgxtasx1smit4yk9mdxr_a-e.png)
Давайте подробнее разберемся, как с этим работать, и начнем с вкладки Quality Settings.
Так выглядят классы «базовых настроек» и «настроек кэша пререндеров» в War Robots. Эти же классы затем используются в рантайме:
Посмотреть код
public class CommonQualitySettings : WRQualitySetting
{
[IntSliderView(0, 72)]
public int CorpsesCount { get; private set; }
[FloatSliderView(5, 600)]
public float UnloadPeriodImGameplay { get; set; }
[FloatSliderView(0, 300)]
public float UnloadPeriodInMenu { get; private set; }
public bool UseMechCacheInHangar { get; set; }
public bool HSEnabled { get; private set; }
public bool BattleAmbientSoundEnabled { get; private set; }
public HangarCacheSettings CacheSettings { get; private set; }
public CommonQualitySettings()
{
CorpsesCount = 12;
UseMechCacheInHangar = true;
HSEnabled = true;
BattleAmbientSoundEnabled = true;
}
}
public class ImageCacheSettings : WRQualitySetting
{
[IntSliderView(0, 1000)]
public int MinCacheSize { get; private set; }
[IntSliderView(0, 1000)]
public int MaxCacheSize { get; private set; }
[IntPopupView(new[] { 128, 256, 512, 1024 })]
public int RenderSize { get; private set; }
public string Info
{
get { return $"Cache takes from {(int) (MinCacheSize * 0.1f)} to {(int) (MaxCacheSize * 0.1f)} Mb"; }
}
public ImageCacheSettings()
{
MinCacheSize = 150;
MaxCacheSize = 200;
RenderSize = 512;
}
}
А вот так это выглядит в окне Quality Manager:
![](https://habrastorage.org/webt/x-/tu/sp/x-tusprqgk5ilqrqtlf8mhaaapi.png)
![](https://habrastorage.org/webt/_g/42/iv/_g42ivwsgj474_wqkqd14qx0b4u.png)
Получается достаточно простая схема. Клиентские разработчики создают класс с набором полей и настраивают уровни качества для него в окне Quality Manager. Он, в свою очередь, «из коробки» умеет отрисовывать примитивные типы, Enum, Nullable, массивы, списки, интерфейсы и собственные классы c полями вышеперечисленных типов, включая другие классы. На случай, когда необходимо отрисовывать для поля кастомный GUI, в QM предусмотрена возможность помечать поля атрибутом, в классе которого реализована отрисовка этого поля.
Так, например, выглядит код класса, меняющего отрисовку для int значения c IntFiled на Slider с параметрами шага, минимального и максимального значений:
Посмотреть код
[Conditional("UNITY_EDITOR")]
public class IntSliderViewAttribute : CustomPropertyViewAttribute
{
public int MinValue { get; private set; }
public int MaxValue { get; private set; }
public int Step { get; private set; }
public new int Value
{
get { return (int) base.Value; }
set { base.Value = value; }
}
public IntSliderViewAttribute(int minValue, int maxValue, int step = 1)
{
MinValue = minValue;
MaxValue = maxValue;
Step = step;
}
#if UNITY_EDITOR
public override void OnGUI()
{
Value = Step * UnityEditor.EditorGUILayout.IntSlider(Value / Step, MinValue / Step, MaxValue / Step);
}
#endif
}
А так — поле с этим атрибутом:
[IntSliderView(0, 72)]
public int CorpsesCount { get; private set; }
В QM сразу включен ряд реализаций для кастомной отрисовки полей: IntSliderView, FloatSliderView, IntPopupView, PresetIndexView, PresetNameView, QualitySettingIndexView, QualitySettingNameView. Этого скромного набора нам хватило для интеграции QM в War Robots и переезда со старого QM на новый. Кода в проекте стало ощутимо меньше, а тот, который остался, стал заметно проще, понятнее и описывал именно то, что он и должен был описывать — данные и логику работы с ними.
У нас уже есть классы настроек качества и данные для их уровней, так что пора формировать из них пресеты. Для этого отправляемся на первую вкладку окна QM — Presets.
![](https://habrastorage.org/webt/xh/pf/px/xhpfpxhwksqcqy_trbobkfnyjju.png)
Тут все достаточно тривиально: мы заводим необходимое количество пресетов, задаем им имена и выставляем для них уровни настроек качества. Готово.
Теперь, когда у нас есть пресеты, осталось задать параметры, по которым будет определяться, какой пресет использовать на девайсе.
Третья вкладка — Device Groups.
![](https://habrastorage.org/webt/zz/av/xe/zzavxehlo6vngnfgwpff3f7oyyo.png)
На этой вкладке мы формируем группы девайсов. Для этого мы используем несколько параметров: объем ОП, частоту процессора, модель GPU и модель девайса для совсем точного попадания. Все параметры не являются обязательными, и можно для группы указать только часть из них. Так, например, для устройств на iOS самый простой вариант — составить карту по модели устройств. На Android же большую часть покроют группы, объеденные по популярным моделям GPU, а в остальных случаях можно указать минимальные требования по объему оперативной памяти и частоте процессора.
Для группы — помимо пресета, который будет выбран по умолчанию — мы также задаем список доступных этой группе устройств пресетов для того, чтобы пользователь на low-end девайсе не смог поменять настройки на ultra high, что может привести к крешам по OOM на старте приложения и блокировать тем самым возможность изменить настройки обратно.
Итак, конфиг QM готов. Сериализуется он в JSON, что дает возможность его легко читать и править без окна QM, а также доставлять на клиент с сервера.
Помимо описанного функционала, QM позволяет:
- работать с несколькими конфигурациями;
- добавлять к пресетам кастомные данные, не относящиеся к уровню настроек качеств (мало ли);
- конфигурировать базовые настройки, не относящиеся к пресетам (раздел Custom Data).
Runtime
API рантайм-части достаточно простой и включает основные методы для работы с QM:
- инициализация (в том числе и обновление текущего инстанса QM из нового конфига);
- выбор пресета (по индексу, имени, объекту пресета из конфига);
- выбор уровня настройки качества (по индексу, имени, объекту настройки качества из конфига);
- сброс пресета на дефолтное значение;
- сохранение/удаление данных о выбранном пресете и уровне настроек качества (стейта).
Так инициализируется инстанс QM:
var exampleStateController = new ExampleStateController();
var qualityManager = QualityManager.Initialize<ExampleQualityManager>(exampleStateController);
Помимо основных методов, инстанс содержит ивенты о смене пресета и уровня настройки качества. Логика применения настроек качества лежит на клиентской стороне — для этого у класса QualitySetting есть виртуальный метод Apply, который вызывается при смене уровня настройки качества.
Помимо базовых методов API, в QM есть набор методов для «мягкого» переключения уровней качества. В случае, когда в рантайме мы сталкиваемся с ситуацией, при которой девайс должен тянуть заданный ему пресет, но при этом происходит падение FPS из-за посторонних факторов, можно ослабить нагрузку, понизив одну из настроек качества, или наоборот. Для этого в QM есть отдельный тип настроек качества, у которого есть виртуальный метод CanBeSwitchedTo, позволяющий определить, можно ли сменить текущий уровень настройки на новый. Так мы можем в рантайме поэтапно даунскейлить качество, чтобы стабилизировать FPS, или наоборот — попробовать дать девайсу нарисовать лучшую картинку, пока не начнем ловить падение FPS.
![](https://habrastorage.org/webt/vm/mf/pv/vmmfpvwwr_mfm3_lqsyzt8cnk3e.png)
Для этого API QM содержит набор методов для попытки даунгрейда или апгрейда уровня настроек качества. При этом можно задать как конкретный тип настройки качества, так и предоставить системе самой решать, какую из них менять. В этом случае система выберет настройку, приводящую к минимальному изменению общего качества, и будет выбирать настройки с большим количеством уровней качества.
При смене пресета/уровня настройки качества QM сохраняет стейт и при последующей инициализации использует уже его. По умолчанию данные сохраняются в PrefsManager, и сохраненный стейт используется до тех пор, пока не будет изменена версия конфига QM. При необходимости можно реализовать свой вариант IStateController и использовать эту реализацию при инициализации QM, чтобы определить, когда можно использовать стейт от старой версии конфига, а когда смена конфига должна приводить к сбросу пресета на дефолтный.
Конфиг для QM лежит в папке ресурсов и по умолчанию грузится из билда. Дополнительно к этому мы предусмотрели механизм удаленного обновления конфига с CDN. Для этого файл конфигурации выкладывается на CDN, а на клиент приходит ссылка на него с указанием хеша конфига. По хешу система определяет, нужно ли ей обновить конфиг перед инициализацией или на девайсе уже есть актуальная версия конфига. Так мы в любой момент при необходимости можем поменять настройки QM у всех пользователей.
С QM разобрались. Давайте теперь более предметно рассмотрим, как мы сформировали список настроек качества, пресетов и групп устройств на проекте War Robots.
Как все это работает в игре
Мы решили зафиксировать несколько четко установленных пресетов, а динамики достигать так же, как и в консольных играх, — за счет скейлинга картинки. Современные девайсы обладают экранами с высоким разрешением, однако GPU в них стоят, конечно, далеко не RTX 3090, так что было бы наивно полагать, что они будут справляться с 60 FPS в нативных Quad HD или даже 4k. Собственно, мы сразу ограничили плотность пикселей сверху, проитерировавшись до значения в 350 ppi.
Изначально при работе над ремастером мы фокусировались на двух качествах — HD (high definition) и LD (low definition). Весь контент, который мы переделывали, основывался на них, и все инструменты по автоматической генерации исходили тоже из них. Однако вскоре мы поняли, что нам понадобится дополнительный уровень качества, который будет нацелен на устройства с небольшим, по нашим меркам, объемом RAM. Так родился еще один пресет качества — ULD (ultra low definition).
Так выглядит игра в качестве HD:
![](https://habrastorage.org/webt/rc/-v/vx/rc-vvxerbeac5e875lxn_tfdmxm.png)
Так — в LD:
![](https://habrastorage.org/webt/an/3a/3q/an3a3qvrpodaorkay87lj7x-ors.png)
А так — в ULD:
![](https://habrastorage.org/webt/gi/ge/iz/gigeizwk8csdu1ihxxmbl339o6w.png)
Каждый пресет мы разграничили не только по качеству, но и по максимально возможному FPS. Сейчас мы позволяем выбирать некоторым устройствам 60 FPS, что достигается благодаря отдельной настройке внутри нового QM:
![](https://habrastorage.org/webt/nd/pt/z1/ndptz1kcw9ft5yu2mgeoxq0snha.png)
Как мы видим, каждый пресет имеет настройку TargetFPSQualitySettings и внутри нее два уровня, отвечающие за максимально возможный FPS на устройстве. Затем «глобальные» пресеты качеств также разделяются: например, есть качество LD, а есть LD60. Это значит, что пользователи, устройства которых попадают в группу LD60 (по названию устройства — на iOS или по GPU — на Android), получают возможность в настройках включить 60 FPS:
![](https://habrastorage.org/webt/0q/9f/lk/0q9flkdbjgbf5osmkqxztvyyqjg.png)
Какое-то время мы рассуждали, нужно ли включать пользователям 60 FPS по умолчанию, но пришли к тому, что не стоит: это значительно повысит использование батареи мобильного устройства, а по нашим внутренним данным на разных проектах на такую частоту кадров переключаются 5-15% аудитории (у которой эта настройка вообще доступна).
Также внутри QM содержится важнейший параметр — с каким «тэгом» ресурсной системы работать:
![](https://habrastorage.org/webt/iu/qe/v0/iuqev0oohq_8yggw98ssagtic1m.png)
Так, для ULD качества используется тег LD_ULD, который содержит набор ресурсов, упакованных нашей ресурсной системой для этих качеств. Объединение этих двух качеств дало нам большую экономию на дубликатах ресурсов, которые складываются в Asset Bundles — но это, я думаю, мы расскажем в наших следующих статьях.
Таким вот образом «собирается» каждое качество: это всего лишь набор уровней настроек.
Для того, чтобы применить настройки в рантайме, используется система наследования классов QM. Разберем пример применения настроек рендера:
![](https://habrastorage.org/webt/4_/eq/pw/4_eqpwhoptn8gvggffarcfzgldm.png)
Как пример, для ULD настройки рендера используется MasterTextureLimit = 1. Давайте посмотрим, как мы можем применить его к нашей игре.
Каждая настройка должна переопределять абстрактный класс WRQualitySetting, что и делает наш пользовательский класс RenderingQualitySetting:
public class RenderingQualitySettings : WRQualitySetting
{
public RenderingPipelineAssetType RenderingPipelineAssetType { get; private set; }
public RenderingPipelineSetting RenderingPipelineSetting { get; private set; }
public int MasterTextureLimit { get; set; }
public MsaaQuality MSAA { get; set; }
// … some code … //
}
Благодаря этому наш класс может перегружать метод Apply:
public override void Apply()
{
base.Apply();
// … some code … //
// We don't want to switch MasterTextureQuality when it is set to 0 and the new quality is
// also using 0 (so i.e. HD -> LD or LD -> HD)
// So effectively we are only doing the switch when the MasterTextureQuality really changes.
// If MasterTextureLimit is > 0 then we switch in any way
if (MasterTextureLimit > 0)
{
UnityEngine.QualitySettings.masterTextureLimit = MasterTextureLimit;
}
// … some code … //
}
Собственно, при вызове этого метода мы можем делать что угодно. В этот момент мы знаем, что наши ресурсы загружены, а рендер-пайплайн уже готов к работе.
Вызов метода Apply происходит в двух случаях:
- инициализация игры — в этот момент мы поднимаем с диска пресет качества, который использует клиент, и применяем его;
- переключение качества в настройках проекта — в этом случае после ряда проверок контроллер окна вызывает незамысловатый код:
private void OnConfirmPopupButtonClick()
{
var supportedPresetData = _supportedPresetsData.Find(x => x.IndexInUiPresetsLists == _selectedPresetIndex.Value);
AnalyticsUtils.QualityPresetChanged(supportedPresetData.QmPreset.Name);
ApplicationContext.QualityService.QualityManager.SetCurrentPreset(supportedPresetData.QmPreset);
// ... reload game … //
}
Вызов API QualityManager ApplicationContext.QualityService.QualityManager.SetCurrentPreset вызовет, в свою очередь, череду изменений внутри конфигураций и в конце переключит и применит все настройки, которые были зарегистрированы в QM.
Наверное, один из самых важных этапов, который был произведен перед переходом на новый QM — это чистка старых параметров качества.
За долгий срок разработки проекта скопилось приличное количество давно неиспользуемых полей или тех, которые имеют минимальное значение для производительности, а также пресетов качества, которые были созданы под устройства, которые мы давно не поддерживаем.
Мы смогли выделить основные необходимые для нас параметры, которыми хотелось управлять, а также разделили их на условные группы:
![](https://habrastorage.org/webt/eu/yl/tj/euyltju2-w1pyjhfjzvci804ej8.png)
Стоит отметить также, как раньше происходил выбор качества, которое нужно выставить на устройстве. Был большой CS-файл, в котором кодом описывались характеристики нужных устройств. На первый взгляд, такой подход может показаться наивным и не гибким, однако у него есть свои плюсы.
На практике довольно сложно законфигурировать все устройства идеально, и бывают ситуации, когда на разных устройствах с одинаковым, казалось бы, SoC, игра ведет себя совершенно по-разному. Могут быть некачественные детали (например, дешевая память с низкими характеристиками) или некорректно написанные драйверы. В этом случае девайс всегда можно выделить отдельно и подобрать настройки под него. При создании QM мы учли подобные случаи, и мы можем производить конфигурацию пресетов не только per-GPU но и per-device (это активно используется, например, на Apple-устройствах).
Отдельно стоит отметить, что у нас имеется возможность как хранить манифест QM внутри игры, так и на CDN, и доставлять его в клиент динамически на старте. Определение наиболее актуального происходит простым определением наличия ссылки на нашем мета сервере. Если она есть, то конфиг всегда берется с сервера. Также на мета-сервере имеется возможность разделять конфиги по версиям клиента, поскольку у нас бесшовные обновления, и несколько версий клиента живут в проде одновременно.
Вместо заключения
Новый Quality Manager дал нам довольно большие возможности: это и мощность управления конфигурациями из старой системы, и простота тестирования, и возможность менять параметры буквально на лету через сервер, и упрощение разработки графического пайплайна. Также QM удобным образом позволил нам разделить настройки качества и выдать хорошую графику на телефонах, которые ее поддерживают, при этом сохранив приближенную к старой на слабеньких устройствах в таком же FPS, как в оригинальной игре.
Авторы материала: Дмитрий Самсонов, Senior Platform Developer, Павел Зинов, Head of Client Department