[Перевод] TDD в геймдеве или «кроличий ад»
TDD в геймдеве применяют довольно редко. Обычно проще нанять тестировщика, чем выделить разработчика для написания тестов — так экономятся и ресурсы, и время. Поэтому каждый успешный пример использования TDD становится интереснее. Под катом перевод материала, где эту технику разработки применили при создании передвижения персонажей в игре ElemenTerra.
Test-driven development или TDD (разработка через тестирование) — это техника разработки ПО, при которой весь процесс разбивается на множество небольших циклов. Пишутся unit-тесты, затем пишется код, который эти тесты проходит, а после делается рефакторинг. И алгоритм повторяется.
Основы TDD
Предположим, мы пишем функцию, которая добавляет два числа. В обычном воркфлоу мы бы просто написали ее. Но для применения TDD нужно начать с создания placeholder-функции и unit-тестов:
// Placeholder-функция, которая дают неверные результаты:
int add(int a, int b){
return -1;
}
// Unit-тесты, которые выдают ошибку, если add не даст правильных результатов:
void runTests(){
if (add(1, 1) is not equal to 2)
throw error;
if (add(2, 2) is not equal to 4)
throw error;
}
Сначала наши unit-тесты не сработают, потому что placeholder-функция возвращает -1 для каждого инпута. Теперь же мы можем правильно выполнить add, чтобы вернуть a + b. Тесты будут пройдены. Может показаться, что это обходной путь, но здесь есть несколько преимуществ:
Если по ошибке написать add как a — b, наши тесты не сработают, и мы сразу же узнаем, как исправить функцию. Без тестов мы можем не поймать эту ошибку и увидеть нестандартную реакцию, которая потребует времени на отладку.
Мы можем продолжать тесты и запускать их в любой момент пока пишем код. Это означает, что если другой программист случайно изменит add, то он сразу же узнает об ошибке — тесты снова не сработают.
TDD в геймдеве
С TDD в геймдеве есть две проблемы. Во-первых, многие игровые функции имеют субъективные цели, которые не поддаются измерению. А во-вторых, тяжело написать тесты, охватывающие все возможности пространства миров, которые полны сложных взаимодействующих объектов. Разработчикам, которые хотят, чтобы движения их персонажей «выглядели хорошо» или физические симуляции «не выглядели дергаными», будет трудно выразить эти метрики в виде детерминированных условий «пройдено/не пройдено».
Тем не менее, техника TDD применима к сложным и субъективным особенностям — например, к движению персонажей. И в игре ElemenTerra мы это сделали.
Unit-тесты против дебаг-уровней
Прежде чем приступить к практике, хочу провести различие между автоматическим unit-тестом и традиционным «отладочным уровнем». Создание скрытых локаций с искусственными условиями — обычное дело в геймдеве. Это позволяет программистам и QA наблюдать за отдельно взятыми событиями.
Секретный уровень отладки в The Legend of Zelda: The Wind Waker
В ElemenTerra таких уровней много: уровень, полный проблемной геометрии для персонажа игрока, уровни со специальными пользовательскими интерфейсами, которые запускают определенные игровые состояния и другие.
Как и unit-тесты, эти уровни отладки могут использоваться для воспроизведения и диагностики ошибок. Но кое в чем они отличаются:
Unit-тесты делят системы на части и оценивают каждую по отдельности, в то время как отладочные уровни проводят тесты в более целостном виде. После нахождения ошибки на отладочном уровне, разработчикам все еще может потребоваться поиск ошибочной точки вручную.
Unit-тесты автоматизированы и должны каждый раз давать детерминированные результаты, в то время как многие отладочные уровни «управляются» игроком. Это создает разницу в сессиях.
Но это не означает, что unit-тесты лучше отладочных уровней. Последние зачастую более практичны. Однако unit-тестирование можно применять даже в тех системах, где оно традиционно не присутствовало.
Добро пожаловать в «кроличий ад»
В ElemenTerra игроки используют мистические силы природы для спасения существ, пострадавших от космической бури. Одной из таких сил является способность прокладывать пути, которые ведут существ к пище и укрытию. Поскольку эти пути представляют собой динамические сетки, созданные игроками, движение существа должно справляться с необычными геометрическими случаями и произвольно сложным рельефом местности.
Движение персонажа — одна из тех сложных систем, где «все влияет на все остальное». Если вы когда-либо делали подобное, то знаете, что при написании нового кода очень легко сломать существующую функциональность. Нужно, чтобы кролики залезали на небольшие выступы? Окей, но теперь они дергаются, поднимаясь на склоны. Хотите, чтобы пути ящериц не пересекались? Сработало, но теперь их типичное поведение испорчено.
Как человек, ответственный за AI и большую часть геймплейного кода, я знал, что у меня нет времени на ошибки-сюрпризы. Я хотел сразу же заметить регресс, поэтому разработка с использованием TDD показалась мне хорошим вариантом.
Следующим шагом стало создание системы, в которой я мог бы легко определить каждый случай передвижения в виде смоделированного теста на «пройдено/не пройдено»:
Этот «кроличий ад» состоит из 18 изолированных коридоров. Каждый с существом и своей трассой, предназначенной для перемещения только в том случае, если работает определенная функция передвижения. Испытания считаются успешными, если кролик способен перемещаться бесконечно долгое время, не застревая. В противном случае — неудачными. Обратите внимание, что мы тестируем только тело существ (pawn в терминах Unreal), а не искусственный интеллект. В ElemenTerra существа могут есть, спать и реагировать на мир, но в «кроличьем аду» их единственная инструкция — бегать между двумя точками.
Вот несколько примеров таких тестов:
1, 2, 3: Свободное движение, статические препятствия и динамические препятствия
8 и 9: Равномерные склоны и неровная местность
10: Исчезающий пол
13: Воспроизведение бага, при котором существа бесконечно вращались вокруг близлежащих целей
14 и 15: Возможность перемещаться по плоским и сложным выступам
Поговорим о сходствах и различиях между моей реализацией и «чистой» TDD.
Моя система была похожа на TDD в этом:
- Я начал работу над функциями с создания тестов, а затем написал код, необходимый для их выполнения.
- Я продолжал выполнять старые тесты, добавляя новые функции.
- Каждый тест измерял ровно одну часть системы, что позволяло мне быстро находить проблемы.
- Тесты были автоматизированы и не требовали инпута игрока.
И отличалась этим:
- При оценке тестов присутствовал элемент субъективности. В то время как настоящие ошибки перемещения (персонаж не дошел от А до В) можно было обнаружить программно. То есть, например, перекос позиции, проблемы синхронизации анимации и дерганного движения требовали человеческой оценки.
- Тесты были не полностью детерминированными. Случайные факторы, вроде колебания частоты кадров, вызывали небольшие отклонения. Но в целом, существа обычно следуют одними и теми же путями и имеют между сессиями одинаковые успехи/неудачи.
Ограничения
Использование TDD для передвижения существа ElemenTerra было огромным плюсом, но мой подход имел несколько ограничений:
- Unit-тесты оценивали каждую особенность движения по отдельности, поэтому ошибки с комбинациями нескольких особенностей не считались. Иногда приходилось дополнять unit-тесты традиционными уровнями отладки.
- У ElemenTerra есть четыре вида существ, но тесты содержат только кроликов. Это особенность нашего производственного графика (остальные три вида были добавлены намного позже в разработку). К счастью, все четверо имеют одинаковые возможности передвижения, но большое тело Mossmork вызвало несколько проблем. В следующий раз я бы сделал, чтобы тесты динамически спавнили выбранный вид вместо использования предварительно размещенных кроликов.
Этот Mossmork требует немного больше пространства в отличие от кролика
TDD — ваш выбор?
Разработчики могут потратить слишком много сил на уровни для unit-тестов, которые игрок никогда не оценит. Не отрицаю, я и сам получил много удовольствия от создания «кроличьего ада». Такие внутренние функции могут отнять много времени и поставить под угрозу более важные майлстоуны. Чтобы этого не случилось — тщательно изучите, где и когда стоит использовать unit-тесты. Ниже я выделил несколько критериев, которые оправдывают TDD для передвижения существа ElemenTerra.
1. Потребуется ли много времени для ручного выполнения тестовых заданий?
Прежде чем тратить время на автоматизированное тестирование, нужно проверить, сможем ли мы оценить функцию с помощью обычных игровых элементов управления. Если вы хотите убедиться, что ваши ключи отпирают двери, заспавните ключ и откройте им дверь. Создание unit-тестов для этой функции было бы потерей времени — ручное тестирование занимает всего несколько секунд.
2. Сложно ли создать тестовые задания вручную?
Автоматизированные unit-тесты оправданы, когда есть известные и трудно воспроизводимые случаи. Тест №7 «кроличьего ада» проверяет, как ходят по уступам — то, чего ИИ обычно сильно старается избежать. Такая ситуация может быть трудной или невозможной для воспроизведения с помощью игровых элементов управления, а тесты — легко.
3. Знаете ли вы, что желаемые результаты не изменятся?
Игровой дизайн полностью основан на итерациях, поэтому цели фичей могут меняться по мере того, как ваша игра будет переделываться. Даже небольшие изменения могут аннулировать метрики, по которым вы оцениваете свои фичи, и, следовательно, любые unit-тесты. Если поведение существ во время еды, сна и взаимодействия с игроком несколько раз менялись, то переход из пункта А в пункт Б оставался неизменным. Поэтому код передвижения и его unit-тесты оставались актуальными на протяжении всей разработки.
4. Вероятно ли, что регрессии останутся незамеченными?
Была у вас ситуация, когда вы завершаете один из последних тасков перед отправкой игры, и вдруг обнаруживаете ошибку, которая ломает правила? Причем в функции, которую вы закончили много лет назад. Игры представляют собой гигантские взаимосвязанные системы, и поэтому вполне естественно, что добавление новой функции B может привести к выходу из строя старой функции A.
Это не так плохо, когда сломанная функция используется повсеместно (например прыжок) — вы должны немедленно заметить поломку механики. Ошибки, обнаруженные в поздней разработке, могут нарушить расписание, а после запуска могут навредить игровому процессу.
5. Худшее, что может произойти при использовании тестов и без них?
Создание тестов — это одна из форм управления рисками. Представьте себе, что вы решаете, покупать ли страховку на транспортное средство. Вам нужно ответить на три вопроса:
- Сколько стоят ежемесячные страховые взносы?
- Насколько вероятно, что автомобиль будет поврежден?
- Насколько дорогим был бы наихудший сценарий развития событий, если бы вы не были застрахованы?
Для TDD мы можем представить себе ежемесячные взносы в виде производственных затрат на обслуживание наших unit-тестов, вероятность повреждения автомобиля в виде вероятности получения бага, а стоимость полной замены автомобиля как наихудший сценарий для регрессионной ошибки.
Если для создания теста фичи требуется много времени, она несложна и вряд ли будет изменена (или с ней можно будет справиться, если она сломается в поздней разработке), то unit-тесты могут доставить больше проблем, чем пользы. Если тесты сделать легко, функция нестабильна и взаимосвязана (или ее ошибки будут займут много времени), то тесты помогут.
Пределы автоматизации
Unit-тесты могут стать отличным дополнением для поиска и устранения ошибок, но они не заменяют необходимости профессионального контроля качества в крупномасштабных играх. QA — это искусство, требующее креативности, субъективного суждения и отличной технической коммуникации.