Автор: Vladimir Tolmachev
Автоматизированное тестирование во всю свою мощь используется многими компаниями. Юнит-тесты, Интеграционные тесты, UI тесты, ручное тестирование и прочие методы. Но почему-то в такой большой области, как GameDev автоматизация тестов сводится к тому, что билды передаются в QA отдел на ручное тестирование. Постараюсь рассказать как разрабатываю игры я, и как пишу для них тесты.
Конечно же, всегда есть исключения и во многих компаниях, я надеюсь, каждый билд все же проходит большинство проверок, прежде чем попасть в QA и тем-более к игрокам. К сожалению, мне не довелось поработать в таких компаниях, хоть и выборка небольшая — всего три компании сменил за свою карьеру. И в каждой из них тестирование проводилось только ручное (а в первой даже тестировщиков не было, обходись силами — «кто фичу сделал, тот ее и проверяет»)
Об игре
Пару слов о игре — мобильная игра Dungeon. Брождение героями по подземельям, сбор различных полезных вещей, выполнение заданий и прокачка этих самых героев. По описанию не сказать, что игра сложная и в ней много различных фич. Но внутри, как оно обычно и бывает, гораздо все сложнее.
Также о внутренностях игры — движок cocos2d-x-3.17, язык разработки — C++. Большинство тестов — Python. Из используемых сторонних инструментов — TexturePacker (сборка атласов), Spine (скелетная 2d-анимация), Tiled (редактор тайловых уровней). Сюда же стоит отнести Google Spreadsheets (большинство данных игры хранятся в таблицах). Целевые платформы — Android, iOS. Разработка ведется на OS X/Windows.
Игра все еще в разработке, уже 1,5 года. Разрабатывается в свободное время и это время ограничено. Потому и пишется много дополнительного кода, позволяющего минимизировать любой рутинный труд
Список тестов, использующих в игре
- Статическая валидация ассетов (верстка, текстуры, прочее)
- Статическая валидация локалей
- Статическая валидация уровней
- Динамическая валидация уровней
- Оценочная проверка баланса юнитов/монетизации в Google-таблицах
- Экспорт данных из Google-таблиц в конфиги игры (с проверками)
- Юнит тестирование core-механик игры
- Юнит тестирование генерации уровней
- Интеграционное тестирование геймплей механик
- UI тесты самой игры
И несколько вещей, которые постоянно запускаются и помогают мне, как разработчику:
- Git-хуки
- Оптимизация include в исходниках
- Соответствие code-style
- Сборка и оптимизация атласов
- Экспорт Spine анимаций
- Firebase TestLab
- Читы
А теперь подробнее о каждом из этих пунктов
Статическая валидация ассетов (верстка, текстуры, прочее)
Учитывая, что разработка ведется на cocos2d-x, и знаком я с ним очень давно, то я не пользуюсь ни одним из их редакторов (Cocos, CocosStudio, CocosBuilder и были еще редакторы вроде как). Не пользуюсь по причине, что они постоянно меняются и их поддержка уходит на нет при обновлении движка на более свежую версию.
Вся верстка игры — xml. Все конфиги — xml. Так уж повелось. Сложно, но универсально. Поддержкой занимаюсь сам, ничего не устаревает, все что необходимо уже написано давно, а если и появляется нужда добавления нового функционала — очень быстро дописывается.
А теперь о том, что проверяется. Каждый объект на сцене описывается одной xml-нодой. Со своими дочерними объектами, свойствами и прочим. Так вот, каждое свойство можно и нужно проверить. Ссылка на текстуру, ссылка на шрифт, указание размера/позиции и прочего. Все просто проверяется, для этого написан небольшой скрипт на Python. Один раз написан, пару раз дополнялся и спасает от всех опечаток, которых хватает при ручном редактировании.
Статическая валидация локалей
С локализацией довольно просто работать, когда она вставляется в игры в конце. Но после этого многое в игре меняется, удаляется и добавляется. Локализаций в игре много, а разработка всегда ведется на одном языке. И если на этом языке можно заметить ошибки при запуске и отладке, то про другие часто забывается.
Для локалей две проверки:
- Каждая указанный id локали есть в списке локалей
- Для каждой локали хранится кеш, связанный с другими языками. Если в базовом языке поменялся текст, то при экспорте все новые изменения покажутся. Каждое изменение можно разрешить как готовое руками, можно полностью для всей таблицы.
Спасает от случаев: к примеру сделано новое окно с текстом — добавлен ID строки, но не сделан экспорт; изменилась строка, но билд все еще разрабатывается и на перевод отдавать рано — предупреждение при разработке, ошибка при сборке продакшн версии билда.
Статическая валидация уровней
С уровнями тоже все достаточно просто. Есть набор базовых условий из которых состоит уровень, к примеру — должны быть точки входа и выхода из уровня. Должен быть между ними путь, который возможно пройти. Должно быть определенное количество объектов, монстров. Все связи между объектами валидны, все объекты на уровне все еще присутствуют в игре.
Все проверки примитивны, но они хорошо спасают при создании, модификации и поддержки уровня. Удалился объект с уровня, а на нем завязана логика — проверка покажет. Переименовался монстр в конфигах игры, а на уровне забыли — проверка покажет.
Динамическая валидация уровней
Для того, чтобы провести достаточно корректную проверку уровня игровыми же средставами, необходим написать режим автоигры. Авторежим должен поддерживать как можно больше игровых механик (желательно все, но про 80/20 лучше не забывать). Примитивный алгоритм автоплеера — найти задачу, создать команду или список команд для ее достижения и начать выполнять их. При выполнении задачи — повторить, пока не будет завершен уровень и на нем не останется других задач. Что может входить в задачи? Самое простое — завершить уровень, но это слишком абстрактно. Для замкнутых пространств, коими и являются подземелья, это сбор предметов, убийство монстров, открытие замков, решение головоломок. Действия же в большинстве своем более просты. Дойти до точки, взаимодействовать с объектом. Большинство всех действий уже есть в игре, достаточно только дергать соответствующие методы.
Могу привести еще пример. Игра жанра Tower Defense, тоже мобильная. Около 100 уровней, по 2 режима с разными настройками, на уровень уходит в среднем 3 минуты. Проверить все уровни займет 10 часов. При ручном тестировании проверяется несколько начальных уровней, выборочно еще 1-2 в середине игры. В результате постоянно случались баги на уровнях, как значительные, так и нет. Причина проста — постоянное изменение игры, ее контента, добавление и удаление контента.
В какой-то момент был написан механизм для тестирования уровней, довольно примитивный:
- Запускается уровень
- Выбирается место для постройки башни (для каждого места можно вычислить коэффициент его «полезности» и выбрать более лучший/худший/средний)
- Выбирается любая башня из доступных и строится на этом месте
- Если есть башня, которую можно апгрейдить — то апгрейд
- Повторять до завершения уровня, после чего запускать следующий.
Данная проверка запускается на высокой скорости игры, все уровни проходятся в среднем за час в автоматическом режиме. Используется на рабочем проекте уже более трех лет, за это время было найдено много ошибок. И как бонус — решили доработать авторежим и вставить его в игру в качестве новой механики. Игрокам понравилось, все довольны.
Оценочная проверка баланса юнитов/монетизации в Google-таблицах
Тут все проще и сводится к условному форматированию с изменением подсветки явно ошибочных ячеек с данными. Много данных для баланса и монетизации игры рассчитываются по формулам, и уже сложно опечататься. К тому же это гораздо удобнее и нагляднее.
Экспорт данных из Google-таблиц в конфиги игры (с проверками)
Для каждой таблицы есть свой export_{table}.py скрипт. Читает данные и записывает в удобный для игры формат. Проверяет, что для каждой таблицы соблюдены свои правила. Сложно охватить общими словами, потому как у каждых данных своя валидация.
Юнит тестирование core-механик игры
Начинается самое интересное — необходимо проверять свою работу и то, что она написана правильно. Тут могу сказать, что все ядро игры написано в ECS и оно довольно просто охватывается юнит тестированием. Признаю честно, бОльшая часть кода тестами не покрыта, тут работает правило — если механика сложная, для нее пишутся тесты. Если простая, то не пишется. Если возник баг при написании или поддержки — то тест уже обязателен, даже на метод получения квадратного корня (утрированно, конечно).
Расчет урона — покрыть тестами. Юнит должен умереть при 0 здоровья — покрыть тестами. Предмет должен купиться или не купиться — покрыть тестами. Возник баг — написать тест, его воспроизводящий и только после этого фиксить проблему. TDD используется не постоянно, но некоторые принципы все же соблюдать полезно.
Юнит тестирование генерации уровней
В какой-то момент устал быть level-дизайнером и озадачился генерацией уровней. Написано несколько тестов, взята уже существующая на этот момент валидация уровней. Можно приступать к реализации алгоритма. Алгоритм через некоторое время готов, а уверенности, что все написано верно и игрок не застрянет на уровне — нет. Посидел, подумал, какие тупики могут быть, добавил еще тестов. Запустил проверку на допустимом диапазоне сидов и ушел спать. На утро много невалидных сидов. Беру каждый из них, получаю уровень, проверяю что же с ним не так и вношу соответствующие правки. После финального коммита правки вносились несколько раз и большинство из них по субъективным причинам — то тут не нравится как получился уровень, то там. Ошибок обнаружено больше не было и это главное.
Интеграционное тестирование геймплей механик
Юнит тестирование — хорошо, а интеграционное еще лучше. Хоть все части игры и работают по отдельности хорошо, но вместе они могут и будут давать сбои. Тут очень хорошо помогло то, что начал писать игру в отрыве от движка (в частности хорошо этому поспособствовал ECS). Можно собрать отдельно билд без движка и проводить быстрые и в тоже время корректные тесты.
На каждую команду, которую может дать игрок своему герою — специальный уровень 5*5 (разные размеры, но минимальные) клеток. Команда — проверка что действие совершено, есть последствия действия и прочее.
На каждое действие в мета игре — покупка предмета, снаряжение героя, прокачка героя, выполнение квеста.
UI тесты самой игры
И самое тяжелое, по времени исполнения, тестирование — это UI тесты. Долго думал как же подступиться к нему. Как тестировать все эти сложные штуки в геймплее, переключение окон и их реагирование на события. Все просто — большинство команд — это нажать на кнопку или часть экрана. Большинство проверок — что такой-то объект на сцене есть и он (не)виден/включен и пр.
Пример тестового сценария:
Это простой тест, открывает окно настроек, нажимает на каждую кнопку в окне и проверяет, что кнопка привлекла действие.
Опишу как он работает:
- При запуске теста строится список команд
- Каждый игровой цикл запускается следующая команда
- Команда может быть проверкой, может быть и действием
- Проверка, в случае неудачи, бросает исключение и игра показывает место на котором зафейлился тест.
- Для CI игра при фейле теста завершается с кодом, отличным от нуля.
Для каждого окна в игре — по своему сценарию. Особо сложные сценарии разбиты на несколько. Такие тесты уже сложно поддерживать, меняется все часто, особенно в начале разработки. Изначально их было написано много, после чего многие пришлось менять, удалять. Надоело и отключил их на время. Когда игра уже приняла окончательный вид (на самом деле она далека еще от этого, но уже стабильности много) вернул, поправил тесты на измененные вещи, удалил неактуальные, добавил новые. Из задержек времени — одного часа достаточно, чтобы зафиксировать поведение сложного окна. На простое же достаточно и 10-15 минут.
Git-хуки
При каждом коммите запускается хук. Строит список тестов, которые необходимо запустить по списку измененных файлов. Но только те тесты, которые выполняются достаточно быстро. Если на хук при коммите уходит больше 5-10 секунд, то это долго. Ускоряю тесты по возможности, пересматриваю их необходимость в этом хуке или для этих файлов.
Оптимизация include в исходниках
Ни для кого не секрет, что чем меньше include в исходниках, тем быстрее идет сборка. Периодически запускается как простой скрипт, убирающий лишние инклюды, так и более сложный, по удалению всех include по одному и проверки собираемости проекта на основных платформах. Запускается раз в неделю на сборочной машине, никак не отвлекает, но довольно часто дает дифы на удаление ненужного из исходников. Мелочь, но приятно. И чуточку быстрее сборки.
Соответствие code-style
Самые минимальные. Пишу код я один, редко ошибаюсь, в основном IDE все делает за меня. Но некоторые проверки все же присутствуют.
Сборка и оптимизация атласов
Перед сборкой каждого атласа проверяется каждый файл на нужность игре. Если он не нужен — переносится в отдельную директорию неиспользуемых ресурсов. Если вдруг атлас становится слишком большим (2048 максимальный рекомендуемый размер для мобильных девайсов)- ошибка. Необходимо переделывать текстуры в нем, уменьшать их размер или переносить в другой атлас. Очень хорошо помогает при компоновке атласов для анимаций — можно перебрать различные варианты смешения текстур юнитов, выбрать наиболее оптимальный вариант и сохранить пользователям немного оперативной памяти.
Экспорт Spine анимаций
Спайн хранит свои файлы в.spine бинарниках, для игр уже может делать экспорт в различные форматы. cocos2d-x использует json, который легко парсится и проверяется. Из каждого json достается список необходимых текстур, список анимаций. Проверяется, что все необходимые анимации есть, на этом же этапе оптимизируется список файлов для атласа. Все это дело собирается воедино и вставляется в игру. Просто, быстро и уменьшение ошибок при ручном экспорте.
Firebase TestLab
При сборке андроид билда, apk отправляется на сервер тестирования Firebase TestLab на случайном девайсе из доступных. Удобный инструмент, неплохой список девайсов с различными версиями OS. Robo-tests не настраивал, проверяю только на успешность запуска игры. На текущем проекте не попадались еще ошибки, и это радует. Тем не менее тест запущен, работает и не мешает. Если отловит ошибку в будущем — то время потрачено не зря.
Читы
Открыть все уровни, открыть весь контент, прокачать героев до максимума, добавить валюты и т.д. С читами разработка становится приятнее и быстрее. Часть читов используется и в автоматизированных тестов. Часть тестов выросли на читах. Без них никуда. На продакшн сборках все читы также доступны, но уже включаются не для каждого пользователя.
CI
Помимо git-hooks и ручного запуска тестов, конечно же лучше всего использовать CI. Изначально хватало bitbucket-pipelines. Но когда стал выходить за рамки месячного лимита, настроил TeamCity на отдельном компьютере. Настроил сборку всех unit, integration тестов на каждый коммит. При сборке на целевую платформу уже прогоняются все тесты. Занимает больше компьютерного времени, чем просто сборка билда, но стабильность этих билдов возрастает в разы.
Заключение
Периодически вылавливаются ошибки, оперативно фиксятся и получаю готовый билд, который уже можно вручную тестировать, и при этом случается крайне мало недочетов и недоработок. В ближайшее время планируем выпустить игру и надеюсь на продолжение статьи, расскажу более подробно о том, как построены процессы разработки, как ускоряю написание кода и для чего может пригодиться кодогенерация.
0 комментариев
Добавить комментарий