Всем привет, меня зовут Алексей Лесовой, я работаю программистом в студии Allods Team. В этой статье я расскажу, как мы с командой искали способ автоматически измерить производительность в Warface, как вырабатывали сценарий и метрики, с какими трудностями столкнулись и к каким результатам пришли.
Организация автотестов
В первую очередь нас интересовал именно статический автотест, то есть такой автотест, по которому можно точно сказать, стала ли игра лучше или хуже по производительности. Поэтому общую идею мы сформулировали следующим образом: «Мы хотим располагать камеру в игровом мире по конкретному алгоритму так, чтобы от запуска к запуску она была в одних и тех же местах. И чтобы при этом у нас была возможность снимать показатели производительности, которые нас интересуют». Как же это сделать?
Все было бы просто, если бы у нас была open world игра с террейном и без вертикального геймплея — тогда найти точку, в которую стоит поставить камеру, не составляло бы труда. Можно было бы просто разбить карту на сетку 10x10, а потом сверху-вниз стрелять рейкастом в центр каждой ячейки и искать террейн. После этого мы поднимали бы камеру до тех пор, пока не появится свободное пространство для игрока (не менее двух метров). Из полученной точки оставалось бы повернуть камеру в четырех направлениях и снять показатели.
При таком подходе, очевидно, могли быть и проблемы. Например, камера могла бы попросту оказаться под водой или в точке, куда игрок попасть никак не мог. Но этого было бы более чем достаточно, чтобы можно было сравнивать производительность клиента игры между версиями.
К сожалению, с Warface все немного сложнее: каждый уровень имеет уникальную структуру, присутствует вертикальный геймплей, да и террейн есть не везде (на самом деле, он есть только на новой PvE-миссии «Каньон: Вторжение»).
Первый заход на организацию автотестов был на проекте Warface: Breakout. Там все упрощалось тем, что в проекте были только PvP-карты, на которых обязательно присутствовали два спавн-поинта, расположенные симметрично относительно центра карты: «SpawnTeam1» и «SpawnTeam2». Имея такие данные, мы уже можем определить размер карты в XY-плоскости.
Давайте рассмотрим две возможные структуры карты.
В первом случае спавн-поинты расположены в углах карты, а во втором — у центров противоположных границ карты. Как же нам учесть оба случая в одном алгоритме, цель которого — определить размеры карты?
Появилось такое решение: нужно построить квадратную зону в XY-плоскости, опираясь на точки спавна.
Вот как выглядит этот алгоритм:
построить единичный вектор a1 от SpawnTeam1 в сторону SpawnTeam2;
построить перпендикулярный вектор a2. Так как мы сейчас работаем в двухмерном пространстве, то это делается просто по формуле a2 = (-a1.y, a1.x);
рассчитать расстояние d между спавн-поинтами;
рассчитать координаты вершин «карты». Например, одна из вершин будет равна SpawnTeam1 + a2 * d/2.
Визуализация выглядит так:
Во втором примере такой подход отлично описывает размеры карты, а в первом (где спавны находятся на углах карты) — мы захватывали лишнее. В любом случае размеры карты определялись более-менее корректно и этого было достаточно.
Но это мы рассмотрели только плоскость XY. А у нас игра с вертикальным геймплеем, где игрок может забираться на коробки, на здания, стрелять вверх или вниз. Как нам все учесть? К тому же из-за разного размера карт плотность расположения точек будет отличаться.
Первая мысль, которая пришла в голову — итерироваться с некоторым шагом как по горизонтали, так и по вертикали. Если границы в плоскости XY мы построили ранее, то для координаты Z можно использовать два доступных для нас показателя: координату Z спавн-поинтов и высоту самого большого объекта. Так мы можем построить границы по координате Z.
Первые проблемы, с которыми мы столкнулись:
на карте могут быть огромные объекты. Измерять производительность в таких высотах попросту не имеет смысла, так как туда игрок не сможет попасть;
точек получается слишком много: тестирование одной карты занимает минимум 120 минут, а половина измерений не имеет никакого смысла, так как камера либо находится в недосягаемой зоне, либо смотрит в какую-то пустоту (например, в чистое небо, так как теперь мы учитываем еще и повороты камеры по вертикали).
Как же нам выкинуть ненужные точки? И вот тут кроется главная особенность автотестов на Warface: Breakout. Карту мы проходим дважды: для генерации тестовых точек и для снятия метрик. Как мы определяем, какая точка нам нужна? По количеству объектов в пределах некоторого радиуса. Но с этим возникла трудность — у нас на картах разное количество объектов. Как определить, какое количество объектов для нас будет приемлемым?
Тут есть еще одна проблема: мы итерировались внутри границ карты с некоторым шагом. Если сделать этот шаг достаточно большим, то при выкидывании какой-то точки можно упустить значительный кусок карты. А если сделать его маленьким, то можно набрать кучу точек, которые будут располагаться слишком близко друг к другу — это не даст нам новой информации, но замедлит само тестирование, так как по этим точкам потом надо будет еще пройтись.
Мы не стали ничего дополнительно усложнять, поэтому сделали простой алгоритм:
генерируем точки с маленьким шагом в заданных нами ранее границах;
считаем, сколько объектов захватывает каждая точка, и записываем это значение;
считаем среднее количество объектов на точку (суммарное количество объектов / количество точек);
сортируем точки по количеству объектов;
удаляем те, у которых объектов меньше среднего значения;
для каждой точки мы проходим отсортированный массив точек уже в обратном порядке и удаляем все соседние точки в некотором радиусе.
Результаты получились хорошие, но сам процесс генерации точек и тест всего одной карты занимают почти 90 минут. Все еще долго. При этом на само тестирование уходит 30-40 минут, что в целом неплохо. Значит, нужно что-то сделать с генерацией точек.
В итоге мы просто вынесли генерацию точек в отдельный процесс (раньше это был отдельный проход), в результате которого формируется файл со сгенерированными точками. Так что для многих старых карт, чьи точки уже сгенерированы и лежат в папке проекта, достаточно просто запустить сам тест.
Напоминаю, что это был Warface: Breakout, процесс тестирования которого упрощен за счет типичной структуры карт. А вот на Warface у нас есть PvE-миссии, спецоперации, карта с террейном, карта с небоскребом. Как это все загнать под общий алгоритм?
На самом деле получилось похожее решение. У нас точно есть одна точка спавна — где появляется игрок. Дальше от нее мы начинаем просто «ходить». Мы «ходим» с некоторым шагом, «прыгаем», проверяем, не столкнулись ли с непроходимым объектом. Если не столкнулись, то продолжаем «ходить», если столкнулись, то забываем про эту точку. По сути, мы строим граф и обходим его в ширину. Вот такой итог у нас получился.
Отсеивание точек сделали по аналогии с Warface: Breakout — проходим по точкам и удаляем соседние в некотором радиусе.
В итоге у нас есть генерация точек, проход по ним и снятие метрик. Осталось только научиться запускать эти автотесты и справиться с анализированием.
Инструменты и метрики
Для начала давайте обсудим метрики, которые нас интересовали.
FPS (frames per second) — считаем, сколько кадров было отрисовано за пять секунд нахождения в точке с текущей ориентацией камеры, и вычисляем на основе этого среднее значение.
Frame time — по сути, обратная величина к FPS, но так как мы изначально считаем время кадра в этой величине, она имеет большую точность и более показательна. Но это работает только в том случае, если мы точно знаем, что какая-то фича могла выиграть нам несколько миллисекунд
GPU time — отличается от FPS тем, что это число показывает, сколько времени прошло от начала первого вызова на отрисовку до окончания ожидания фенса;
Drawcalls — общие и по теням. Движок считает это при помощи внутреннего механизма, который мы периодически правим и дописываем. Нам остается только использовать результаты его расчетов;
Потребляемая память — на ОС Windows мы вытаскиваем эти данные из GlobalMemoryStatusEx.
Но недостаточно просто замерить производительность, ее надо как-то интерпретировать. А еще лучше, если получится посмотреть на проблемный кадр. Для этого есть специальный инструмент.
Он запускает клиент игры и окно, написанное на C# WinForms. В окне отображается список тестовых точек, их координаты, а по нажатию на конкретную точку высвечиваются соответствующие метрики. После этого камера в клиенте игры перемещается в эти координаты (с учетом поворота). Красным подсвечиваются точки, метрики которых не соответствуют целевым метрикам (но на данный момент мы еще не до конца определились с некоторыми целевыми метриками).
Кстати, запускается этот инструмент так же, как и генерация точек или само тестирование — через инструмент, который тоже написан на C# WinForms.
В нем можно задавать и бины, на которых хотим тестировать, и настройки графики, и сам сценарий (что конкретно хотим запустить), и карты, на которых надо выполнить этот сценарий.
Почему для всего этого мы использовали C# WinForms? Потому что это самый быстрый и легкий способ написать инструмент с GUI для ОС Windows. Мы точно знаем, что этим инструментом будут пользоваться только наши разработчики, а запускать его будем только на Windows. Поэтому задумываться о том, чтобы сделать этот инструмент на другой технологии — нет смысла.
Что еще не обсудили
Во-первых, тестовые стенды. Очень важно, чтобы тесты проводились на ПК, которые соответствуют определенным графическим настройкам. Нет смысла проверять минимальные настройки Warface на ПК с i9-9900 и RTX 3070: и так понятно, что там все будет в порядке.
Во-вторых, нет какого-то сравнительно анализа между версиями. Мы же не знаем, стал ли перфоманс игры лучше или хуже после предыдущей версии. Для этого мы планируем добавить нашим инструментам возможность делать краткую сводку по версии и загружать ее куда-то, где можно хранить и сравнивать результаты. Примерная идея такая: завести таблицы и графики для каждой карты, где можно будет посмотреть на динамику средних, минимальных и максимальных значений между версиями. И дальше грузить это на выбранный сервис.
В итоге у нас получилась хорошая система статических автотестов. К сожалению, она не показывает динамику (взрывы, звуки, выстрелы). Но в таком виде она даже полезнее, так как можно с уверенностью сравнивать версии игры между собой. Кстати, динамические автотесты можно реализовать на основе системы реплеев. Но это уже совсем другая история.
На этом у меня все. Если есть вопросы, буду рад ответить на них в комментариях.