Менеджер качества, или как не спалить лоу-энд девайсы ультра-графикой

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

Релиз War Robots состоялся еще в 2014 году, и за 7 лет существования проекта графическая часть в нем постоянно развивалась. Но в то же время команда постоянно сталкивалась с ограничениями из-за минимальных требований к девайсам. Оперируя таким большим проектом, у которого немало устройств входит в low-end сегмент, нельзя просто взять и запилить крутой современный графен, не потеряв при этом часть аудитории.

-u4uows8lchailutrnjgom3vt5s.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, какие задачи он решает и как устроен.

uuy2il21ygdwppcvk4_w6r6anam.png


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

Главными сущностями у нас являются:

  • QualitySetting — набор параметров, объединенных в общую группу;
  • Preset, состоящий из выбранных уровней QualitySetting разного типа.

Quality Manager состоит из двух частей: runtime-часть с API для инициализации и переключения качеств и editor-часть с GUI, которые позволяют все это конфигурировать.
Начнем с editor-части. Ее задача — предоставление интерфейса для конфигурации настроек качества и пресетов и минимизация количества кода, необходимого со стороны клиента. В идеале мы хотели заставить клиентщиков описывать только структуры настроек качества, а всю работу по отрисовке оставить на стороне Quality Manager.

Вот так выглядит наше окно, разделенное на три вкладки: настройки качества, пресеты, группы устройств:

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:

x-tusprqgk5ilqrqtlf8mhaaapi.png

_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.

xhpfpxhwksqcqy_trbobkfnyjju.png

Тут все достаточно тривиально: мы заводим необходимое количество пресетов, задаем им имена и выставляем для них уровни настроек качества. Готово.

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

Третья вкладка — Device Groups.

zzavxehlo6vngnfgwpff3f7oyyo.png

На этой вкладке мы формируем группы девайсов. Для этого мы используем несколько параметров: объем ОП, частоту процессора, модель GPU и модель девайса для совсем точного попадания. Все параметры не являются обязательными, и можно для группы указать только часть из них. Так, например, для устройств на iOS самый простой вариант — составить карту по модели устройств. На Android же большую часть покроют группы, объеденные по популярным моделям GPU, а в остальных случаях можно указать минимальные требования по объему оперативной памяти и частоте процессора.

Для группы — помимо пресета, который будет выбран по умолчанию — мы также задаем список доступных этой группе устройств пресетов для того, чтобы пользователь на low-end девайсе не смог поменять настройки на ultra high, что может привести к крешам по OOM на старте приложения и блокировать тем самым возможность изменить настройки обратно.

Итак, конфиг QM готов. Сериализуется он в JSON, что дает возможность его легко читать и править без окна QM, а также доставлять на клиент с сервера.

Помимо описанного функционала, QM позволяет:

  • работать с несколькими конфигурациями;
  • добавлять к пресетам кастомные данные, не относящиеся к уровню настроек качеств (мало ли);
  • конфигурировать базовые настройки, не относящиеся к пресетам (раздел Custom Data).

API рантайм-части достаточно простой и включает основные методы для работы с QM:
  • инициализация (в том числе и обновление текущего инстанса QM из нового конфига);
  • выбор пресета (по индексу, имени, объекту пресета из конфига);
  • выбор уровня настройки качества (по индексу, имени, объекту настройки качества из конфига);
  • сброс пресета на дефолтное значение;
  • сохранение/удаление данных о выбранном пресете и уровне настроек качества (стейта).

Так инициализируется инстанс QM:
var exampleStateController = new ExampleStateController();
var qualityManager = QualityManager.Initialize(exampleStateController);

Помимо основных методов, инстанс содержит ивенты о смене пресета и уровня настройки качества. Логика применения настроек качества лежит на клиентской стороне — для этого у класса QualitySetting есть виртуальный метод Apply, который вызывается при смене уровня настройки качества.

Помимо базовых методов API, в QM есть набор методов для «мягкого» переключения уровней качества. В случае, когда в рантайме мы сталкиваемся с ситуацией, при которой девайс должен тянуть заданный ему пресет, но при этом происходит падение FPS из-за посторонних факторов, можно ослабить нагрузку, понизив одну из настроек качества, или наоборот. Для этого в QM есть отдельный тип настроек качества, у которого есть виртуальный метод CanBeSwitchedTo, позволяющий определить, можно ли сменить текущий уровень настройки на новый. Так мы можем в рантайме поэтапно даунскейлить качество, чтобы стабилизировать FPS, или наоборот — попробовать дать девайсу нарисовать лучшую картинку, пока не начнем ловить падение FPS.

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:

rc-vvxerbeac5e875lxn_tfdmxm.png

Так — в LD:

an3a3qvrpodaorkay87lj7x-ors.png

А так — в ULD:

gigeizwk8csdu1ihxxmbl339o6w.png

Каждый пресет мы разграничили не только по качеству, но и по максимально возможному FPS. Сейчас мы позволяем выбирать некоторым устройствам 60 FPS, что достигается благодаря отдельной настройке внутри нового QM:

ndptz1kcw9ft5yu2mgeoxq0snha.png

Как мы видим, каждый пресет имеет настройку TargetFPSQualitySettings и внутри нее два уровня, отвечающие за максимально возможный FPS на устройстве. Затем «глобальные» пресеты качеств также разделяются: например, есть качество LD, а есть LD60. Это значит, что пользователи, устройства которых попадают в группу LD60 (по названию устройства — на iOS или по GPU — на Android), получают возможность в настройках включить 60 FPS:

0q9flkdbjgbf5osmkqxztvyyqjg.png

Какое-то время мы рассуждали, нужно ли включать пользователям 60 FPS по умолчанию, но пришли к тому, что не стоит: это значительно повысит использование батареи мобильного устройства, а по нашим внутренним данным на разных проектах на такую частоту кадров переключаются 5–15% аудитории (у которой эта настройка вообще доступна).

Также внутри QM содержится важнейший параметр — с каким «тэгом» ресурсной системы работать:

iuqev0oohq_8yggw98ssagtic1m.png

Так, для ULD качества используется тег LD_ULD, который содержит набор ресурсов, упакованных нашей ресурсной системой для этих качеств. Объединение этих двух качеств дало нам большую экономию на дубликатах ресурсов, которые складываются в Asset Bundles —, но это, я думаю, мы расскажем в наших следующих статьях.

Таким вот образом «собирается» каждое качество: это всего лишь набор уровней настроек.

Для того, чтобы применить настройки в рантайме, используется система наследования классов QM. Разберем пример применения настроек рендера:

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 — это чистка старых параметров качества.

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

Мы смогли выделить основные необходимые для нас параметры, которыми хотелось управлять, а также разделили их на условные группы:

euyltju2-w1pyjhfjzvci804ej8.png

Стоит отметить также, как раньше происходил выбор качества, которое нужно выставить на устройстве. Был большой CS-файл, в котором кодом описывались характеристики нужных устройств. На первый взгляд, такой подход может показаться наивным и не гибким, однако у него есть свои плюсы.

На практике довольно сложно законфигурировать все устройства идеально, и бывают ситуации, когда на разных устройствах с одинаковым, казалось бы, SoC, игра ведет себя совершенно по-разному. Могут быть некачественные детали (например, дешевая память с низкими характеристиками) или некорректно написанные драйверы. В этом случае девайс всегда можно выделить отдельно и подобрать настройки под него. При создании QM мы учли подобные случаи, и мы можем производить конфигурацию пресетов не только per-GPU, но и per-device (это активно используется, например, на Apple-устройствах).

Отдельно стоит отметить, что у нас имеется возможность как хранить манифест QM внутри игры, так и на CDN, и доставлять его в клиент динамически на старте. Определение наиболее актуального происходит простым определением наличия ссылки на нашем мета сервере. Если она есть, то конфиг всегда берется с сервера. Также на мета-сервере имеется возможность разделять конфиги по версиям клиента, поскольку у нас бесшовные обновления, и несколько версий клиента живут в проде одновременно.


Новый Quality Manager дал нам довольно большие возможности: это и мощность управления конфигурациями из старой системы, и простота тестирования, и возможность менять параметры буквально на лету через сервер, и упрощение разработки графического пайплайна. Также QM удобным образом позволил нам разделить настройки качества и выдать хорошую графику на телефонах, которые ее поддерживают, при этом сохранив приближенную к старой на слабеньких устройствах в таком же FPS, как в оригинальной игре.

Авторы материала: Дмитрий Самсонов, Senior Platform Developer, Павел Зинов, Head of Client Department

© Habrahabr.ru