[Когда график поджимает и проект уже пора выпускать, программисты могут прибегать к грязным трюкам, чтобы уже наконец выпихнуть игру за дверь. В этой статье собрано девять примеров таких «костылей» из реальной жизни.]
Обычно программисты — это методичные и аккуратные существа, всеми силами стремящиеся к чистому и красивому коду. Но когда ставки высоки, идеальный график разваливается на части, а игру пора выпускать, принцип «закончить любой ценой» может оказаться важнее элегантности.
В подобных случаях измученный и перерабатывающий программист скорее всего проигнорирует оптимальный подход, заменив его менее приемлемым решением, чтобы просто покончить с игрой. Мы собрали девять историй настоящих разработчиков о тех моментах, когда они не могли уложиться в график и им приходилось для спасения проекта прибегать к хитростям.
Сними меня в самом хорошем ракурсе
Примерно четыре года назад [прим. пер.: статья написана в 2009 году] я работал программистом в многоплатформенном проекте для PlayStation 2, Xbox и GameCube. Сроки разработки подходили к концу, потому неудивительно, что в код начали проникать хаки. В версии для PS2 нашлась поздно обнаруженная проблема, которую было очень трудно отследить: при длительном тесте стабильности первого уровня со стоящим неподвижно персонажем игра «вылетала». К сожалению, эта ошибка возникала только в дисковой сборке для розничной продажи, в которой не было никакой отладочной информации. Боясь отказа в публикации от отдела проверки технического соответствия (technical requirement check, TRC) Sony, мы усердно трудились на решением.
Чтобы сузить границы возникновения ошибки, мы постоянно рендерили границу вокруг края экрана с отдельными цветами для разных частей кода. Например, синим обозначалась настройка рендеринга, зелёным обозначалось обновление управления игрока. Поскольку сбои возникали не всегда, ведущий инженер и я вручную прожигали кучу дисков и запускали их на множестве консолей PS2. (Надо учесть, что компания была независимым разработчиком, у неё не было ИТ-отдела и крутого дискозаписывающего оборудования.) Тестовый цикл — изменение кода, запись дисков, запуск и ожидание для сужения приблизительных границ возникновения ошибки — занимал долгие часы. Это были последний этап разработки, и мы могли выполнить только строго ограниченное количество таких циклов.
Проводя выходные дни в офисе в ожидания сбоя, мы убивали время за World of Warcraft. Вдруг кто-то из нас заметил, что игра не «вываливается», если повернуть камеру на 90 градусов вправо. Сначала программисты справедливо отмахнулись от этого как от случайности, скрывающей более глубокую ошибку. Исправление такого поведения не устранило бы корень проблемы. Однако мы всё равно исправили его! Когда у нас кончилось время, мы создали последний диск PS2 с повёрнутой в начале уровня камерой (версии для двух других платформ уже были готовы) и отправили на проверку. Игра прошла все тесты владельцев платформ и была вовремя выпущена на рынок.
Я не назвал бы это решение «трюком в коде», но оно определённо было «грязным». Мистика этого исправления до сих пор не раскрыта, но, к счастью, я ни разу не слышал, чтобы пользователи жаловались на такую проблему.
— Марк Кук (Mark Cooke)
Кризис самоопределения
Эта ситуация знакома всем разработчикам игр: сегодня мы отправляем на «золото» нашу игру для Xbox 1. Команда тестирует игру весь день, желая убедиться, что всё в порядке. Все радостны и спокойны, для нас работа уже закончена.
После обеда мы создаём последнюю сборку с небольшими последними настройками игрового баланса и начинаем последний сеанс тестирования, после чего происходит катастрофа: игра упорно «вылетает»! Мы все бежим к своим рабочим машинам, запускаем отладчик и пытаемся выяснить, что же происходит. Это не что-то тривиальное, вроде assert, и даже не то, что относительно сложно отследить, типа деления на ноль. Похоже, что в нескольких фрагментах памяти находится «мусор», но проверка памяти сообщает, что всё в порядке. Что происходит?
Много часов спустя наши мечты о выпуске в срок разрушены. Мы пытаемся найти ошибку и обнаруживаем, что один файл данных загружается с неверными данными. Неверные данные? Как такое возможно? Наша система ресурсов управляет каждым ресурсом с помощью 64-битного идентификатора, состоящего из CRC32 полного имени файла и CRC32 всего содержимого файла. Таким же способом мы объединяли одинаковые файлы ресурсов игры в один. У нас были десятки тысяч файлов и два года разработки, при этом конфликты никогда не возникали. Никогда.
До этого самого момента.
Оказалось, что одно из невинных небольших исправлений, которые дизайнеры проверяли после обеда, приводило к тому, что текстовый файл имел абсолютно такое же имя и CRC, что и другой файл ресурсов, несмотря на то, что они были совершенно разными!
Когда мы обнаружили проблему, у нас сердце ушло в пятки. Не было никаких шансов изменить систему индексирования ресурсов за такой короткий промежуток времени. Даже если бы мы остались работать на всю ночь, мы никак не смогли бы узнать, будет ли всё стабильно утром.
Так же быстро, как нас охватило отчаяние, возникло и осознание того, как мы можем исправить ошибку вовремя и успеть к выпуску на «золото». Мы открыли виноватый в конфликте текстовый файл, добавили в конце пробел и сохранили его. Взглянули друг на друга с широкими улыбками на лицах и сказали:
«Отправляем!»
— Ноэль Льопис (Noel Llopis)
Езда в нетрезвом виде
Мне дали задание поработать над системой управления транспортом, которая была важной частью нашей игры. К счастью, мы могли взять бóльшую часть кода у другой студии. К несчастью, код не всегда казался идеальным, как это и бывает почти со всем кодом, который не пишешь сам. Я обнаружил этот милый фрагмент кода, который получает переменную из цикла движка, но делает это максимально тупым способом (см. листинг 1).
//**************************************************
// Function: AGameVehicle::Debug_GetFrameCount
//
//! Очень грязный способ получения текущего количества кадров; переменная защищена.
//!
//! Возвращает текущее количество кадров.
//**************************************************
UINT AGameVehicle::Debug_GetFrameCount()
{
BYTE* pEngineLoop = (BYTE*)(&GEngineLoop);
pEngineLoop += sizeof( Array<FLOAT> ) + sizeof( DOUBLE );
INT iFrameCount = *((INT*)pEngineLoop);
return iFrameCount;
Самое смешное в этом фрагменте невероятно отвратительного кода заключается в том, что у объекта была простая функция для получения количества кадров. Но даже если её бы и не было, то написавший этот код мог легко добавить её самостоятельно! Не нужно говорить, что когда я указал на ошибку, этот код не использовался ни в моей игре, ни в той, откуда он был взят. И если это не пример того, почему стоит делать анализ кода, то я уж и не знаю, какие примеры ещё нужны.
— Остин Макги (Austin McGee)
Десятичный код
Работая в [компании X], я думал, что мы никогда не доберёмся до завершения [проекта]. На одном из уровней у нас находился объект, который должен был быть скрытым. Нам не хотелось повторно экспортировать уровень и мы не использовали имена с контрольными суммами. Поэтому прямо посреди кода движка у нас было что-то вроде такого. И игра вышла с этим кодом.
if( level == 10 && object == 56 )
{
HideObject();
Где-то год спустя к нам подошёл очень расстроенный художник, использовавший наш движок, и спросил, почему на его уровне, оказавшемся десятым, не отображается объект. Действительно, почему?
— Аноним
Все знаки говорят «нет-нет»
Эта проблема возникла при разработке нового Wolfenstein Raven Software. Я писал настройку поддержки контроллера для Xbox 360. Оказалось, что при интеграции с Live необходимо знать, какой именно контроллер передаёт события ввода. Использованный нами код ввода Doom 3 практически полностью был скопирован из Quake 3, поэтому это была довольно простая система.
В существующей системе событий каждое событие передавалось с двумя целочисленными аргументами и пустым указателем на случай, если понадобятся дополнительные параметры. Поэтому задача заключалась в том, чтобы связать ID контроллера с каждым событием ввода. Вроде бы, никаких проблем — просто упакуем ID контроллера в один из целочисленных аргументов события, так ведь? Ну и конечно, оба аргумента были уже заняты.
Поэтому к нам пришла другая идея: просто используем наш быстрый покадровый распределитель памяти без фрагментации для выделения памяти, запишем в неё идентификатор контроллера, а затем передадим указатель на него в указателе события.
Выяснилось, что система событий после обработки события самостоятельно очищает с помощью free() нулевой указатель события. Разумеется, такое поведение было совершенно несовместимо с нашим multiheap-подходом. Более того, в коде присутствовали старые фрагменты кода Doom 3, которые полагались на вызов free() в коде события, поэтому мы не могли просто удалить вызов без внесения нетривиальных изменений во всю базу кода. А если добавить третий целочисленный параметр? Тогда придётся изменять несколько сотен вызовов функций.
Дедлайн был уже на носу, и у меня не было времени на решение проблемы. Поэтому я решился на немыслимое — упаковал ID контроллера в параметр указателя. В четырёхстрочном комментарии, целиком набранном «капсом», я пометил это как ужасный хак, и загрузил в базу кода. И этот костыль работал без проблем, пока вся система ввода не была заменёна чем-то немного более хорошим.
— Дэвид Дайнермен (David Dynerman)
Липкие жулики
Вот история проекта из ранних дней PS2. У нас была куча проблем с коллизиями/границами, которые как правило решались в последнюю минуту переписыванием системы коллизий персонажа. Её меняли на модель «коллайдеров» — набор сфер, который гораздо лучше обрабатывал столкновения, чем наше иерархическое дерево ориентированных граничных контуров (то были времена до появления движка Havok).
Однако у нас постоянно возникал редкий «баг», который сводил с ума. Мы называли его «липучестью» — каждый раз, когда персонаж игрока находился рядом со стеной и скользил вдоль неё, сфера коллайдера внезапно решала, что он находится на другой стороне стены, и не давала оторваться от неё (то есть персонаж прилипал к поверхности).
Это был одна из тех проклятых ошибок «вроде бы всё просто». Нужно всего лишь выяснить, почему сфера считает, что находится на другой стороне, и исправить это. Проблема в том, что нужно отследить, что происходит во всех случаях распознавания коллизий перед возникновением этой проблемы.
Когда нам не удалось решить проблему удобными условными контрольными точками в коде, отслеживающими странные отклики, или изменением положения, запутывавшим коллайдер, мы просто снизили частоту кадров, чтобы «баг» просто не возникал. (Настоящее решение этой проблемы потребовало бы крови, пота, слёз и других жидкостей, без которых обычно можно обойтись, но это уже другая история.)
Мы отправляли игру на тестирование и постоянно получали ответ, что игра с такой ошибкой выпущена не будет. Даже придумали одно «решение» — увеличить границы сферы настолько, чтобы гарантировать отсутствие проблем, но это никак нельзя назвать правильным исправлением. Мы перенаправили игру с этим «багом» команде художников, чтобы они исправляли его, и выгадали себе драгоценное время на поиск истинной, глубинной проблемы.
Тестеры начали отправлять нам отзывы типа «игрок сталкивается с невидимой стеной, такого быть не должно». Эта проблема того же уровня серьёзности (мы снова сузили границу коллайдера и персонаж стал близко подходить к стене, потому что «липучесть» возникала очень редко и её было трудно воспроизвести), но после пары таких циклов, за которые мы исправили остальную часть игры, решение, требовавшее крови, пота и слёз, было наконец найдено, и игра продолжила свой путь в тираж и на полки магазинов.
Не скажу, что горжусь этим циклом решения проблем путём избегания и увёрток, но, по крайней мере, нам удавалось временно снять эту ношу с плеч. Мы всегда говорили «ой, нет, мы вчера уже отправили игру на тестирование», и продолжали решать чёртову проблему.
— Аноним
Я и мои патчи
Есть старый анекдот, примерно такой:
Больной: «Доктор, у меня болит, когда делаю вот так».
Доктор: «Тогда не делайте так».
Забавно, но разве в определённой ситуации это не мудрые слова? Представьте мою боль, когда я работал над портированием трёхмерного шутера от третьего лица с PC на первую PlayStation.
Начнём с того, что у PS1 не было поддержки чисел с плавающей запятой, поэтому портирование выполнялось рекомпилированием кода с заменой всех значений с плавающей запятой на значения с фиксированной запятой. И всё получалось довольно неплохо, пока дело не дошло до распознавания столкновений.
В PC-версии игры геометрия уровней работала хорошо, но при преобразовании в значения с фиксированной запятой из-за микроскопических отличий между плавающей и фиксированной запятой проявлялись все швы, Т-образные соединения и другие проблемы. Эта сложность давала о себе знать: основной персонаж (по имени Damp) просто проваливался сквозь эти крошечные отверстия в пустоту под уровнем.
Мы исправили все найденные дыры, настроив геометрию таким образом, чтобы Damp больше не проваливался. Но когда игру отправили на тестирование издателю, он внезапно сообщил нам о множестве ошибок «проваливания сквозь мир». Каждый день находился новый список мест, через отверстия в которых мог провалиться Damp. Мы устраняли их, исправляя геометрию, а на следующий день нам сообщали ещё о десятке других мест. Так продолжалось несколько дней. Отдел тестирования издателя нанял одного работника, чьей единственной задачей были прыжки по миру по десять часов в день для поиска мест, в которые можно провалиться.
Проблема заключалась в том, что геометрия была плоха. Она не была плотной и бесшовной. Её было достаточно для PC, но не для PS1, где математика с фиксированной запятой сильно усложняла проблемы. Идеальным решением было бы исправление геометрии, делающее её бесшовной.
Однако это трудоёмкая задача, которую с нашими ограниченными ресурсами невозможно было бы выполнить в срок, поэтому мы положились на отдел тестирования, чтобы он искал для нас проблемные места.
Проблема в этом случае была в том, что они постоянно находили такие места. Каждый день приносил ещё больше боли. Каждый день — новые вариации старого «бага». Казалось, что это никогда не закончится.
Наконец, до нас дошло: истинная проблема не в дырах геометрии. Проблема в том, что Damp проваливается в эти дыры. Поняв это, я смог написать очень быстрый и простой фикс, который выглядел примерно так:
IF (Damp проваливается сквозь дыру()) THEN
Не проваливаться
На самом деле код был ненамного сложнее (см. листинг 2).
Листинг 2: я и мои патчи
damp_old = damp_loc;
move_damp();
if (NoCollision())
{
damp_loc = damp_old;
Тысяча ошибок была исправлена одним махом. Теперь вместо того, чтобы проваливаться сквозь уровень, проходя над дырами, Damp просто слегка подёргивался. Мы выяснили причину наших страданий и перестали «так делать». Издатель уволил своего тестера-«прыгуна», и игра была выпущена.
То есть выпущена через какое-то время. Вдохновлённый успехом подхода «if A==плохо then NOT A», я воспользовался этим инструментом для патчинга ещё нескольких ошибок. Почти все они были связаны с кодом распознавания столкновений. Ближе к концу разработки ошибки становились всё более и более специфичными, а исправления больше походили на «не делать в_точности_это_действие» (см. листинг 3: это настоящий код, оставшийся в игре).
Листинг 3: я и мои патчи
if (damp_aliencoll != old_aliencoll &&
strcmpi("X4DOOR",damp_aliencoll->enemy->ename)==0 &&
StartArena == 6 && damp_loc.y<13370)
{
damp_loc.y = damp_old.y; // никогда не давать
damp'у прикасаться к двери. (перемещаем x и y)
damp_loc.x = damp_old.x;
damp_aliencoll = NULL; // в таком вот аксепте
Что делает этот код? Проблема возникала, когда Damp касался определённого типа дверей на определённом уровне и в определённом месте, и вместо исправления первопричины я сделал так, чтобы при касании двери Damp отодвигался от неё и притворялся, что ничего не произошло. Проблема решена.
Оглядываясь в прошлое, я нахожу этот код довольно устрашающим. Он патчил ошибки, а не исправлял их. К сожалению, настоящим решением была бы переделка геометрии всей игры и системы распознавания столкновений с учётом особенностей вычислений PS1. С самого начала график был очень агрессивным, поэтому нам всегда казалось, что конец уже близко. Естественно, в такой ситуации быстрый патч всегда имел преимущество над сложным и затратным решением проблем.
Но всё прошло не так гладко. Требовались сотни патчей, потом сами патчи становились причинами проблем, и приходилось добавлять ещё патчей, чтобы изменить поведение патчей в очень специфических случаях. Появлялись новые ошибки, и я снова побеждал их патчами. В конце концов я победил, однако ценой стали задержка выпуска игры на несколько месяцев и моя ежедневная 14-часовая работа все эти месяцы.
Этот опыт настроил меня против «заплаток». Теперь я всегда стремлюсь докопаться до корней ошибки даже при наличии простого и кажущегося безопасным патча. Я хочу, чтобы мой код был здоровым. Когда вы идёте к врачу и говорите «у меня болит, когда делаю вот так», то ждёте, что он выяснит причину боли и вылечит её. Боль, как и ошибки в коде, может быть симптомом чего-то гораздо более серьёзного. Мораль: обращайтесь со своим кодом так, как с вами должен обращаться врач.
— Мик Уэст (Mick West)
В гневе я страшен
Однажды я работал в студии THQ Relic Entertainment над The Outfit, которую вы можете помнить как одну из первых игр для Xbox 360. Мы начали с движка для PC (однопоточного) и хотели примерно за 18 месяцев превратить его в законченную игру для многоядерной консоли нового поколения. Примерно за три месяца до выпуска игры она работала на 360 со скоростью примерно 5 FPS. Было очевидно, что игре нужна серьёзная оптимизация.
Замерив производительность, я понял, что кроме медленности и «PC-стиля» кода есть также куча проблем с контентом. Некоторые модели были слишком детализированными, шейдеры — слишком затратными, а на некоторых уровнях слишком много персонажей.
Сложно убедить команду из ста человек, что программисты не могут просто «исправить» производительность движка, и что необходимо изменить шаблоны работы, к которым многие привыкли. Им нужно было осознать, что производительность игры — проблема для всех, и я придумал лучший способ показать это шуткой, в которой была доля истины.
Решение заняло около часа. Коллега-программист сделал четыре фотографии моего лица: я счастлив, спокоен, немного рассержен, рву на себе волосы. Я поместил фотографию в угол экрана и привязал её к частоте кадров. Когда в игре было больше 30fps, я был счастлив, когда ниже 20, я злился.
После этих изменений подход к проблеме FPS полностью изменился: вместо «Ой, да это проблема программистов» «Хм, если я использую эту модель, то Ник рассердится! Лучше немного её оптимизировать». Люди постоянно видели, как вносимые ими изменения влияют на частоту кадров, и в результате мы выпустили игру с 30fps.
— Ник Вандерс (Nick Waanders)
Антигерой программирования
Я только что выпустился из колледжа, был молод и наивен. Мы как раз переходили к стадии бета-версии моего первого профессионального проекта — игры для PC конца 90-х. Это было увлекательное катание на американских горках, как часто происходит с проектами. Весь контент уже был готов, и игра выглядела неплохо. Однако мы нашли одну проблему: не удавалось уместиться в объём доступной памяти.
Поскольку бóльшая часть памяти была занята моделями и текстурами, мы с художниками стремились как можно сильнее снизить занимаемый ими объём. Мы уменьшали масштаб изображений, упрощали модели и сжимали текстуры. Иногда художники нам помогали, иногда противились всеми силами.
Мы урезали мегабайт за мегабайтом, и через несколько дней безумного напряжения достигли точки, в которой сделать было ничего уже невозможно. Хоть мы и урезали часть важного контента, освободить больше памяти мы никак бы не смогли. Измученные, мы замерили текущий объём используемой памяти. Он был на полтора мегабайта больше предельно допустимого!
В этот момент один из самых опытных программистов нашей команды, переживший многолетний опыт разработки в «старые добрые времена», решил взять решение в свои руки. Он позвал меня в свой офис и мы начали работу, которую я сначала представлял ещё одним выматывающим марафоном для освобождения памяти.
Вместо этого он открыл файл исходника и показал мне эту строку:
static char buffer[1024*1024*2];
«Видишь?» — спросил он меня. И тут же удалил её одним нажатием. Готово!
Наверно, он увидел ужас в моих глазах, поэтому объяснил, что сам выделил эти два мегабайта памяти в самом начале разработки. По своему опыту он знал, что почти никогда невозможно уложиться в нужный объём памяти, и что многие проекты из-за этого проваливались. Поэтому он всегда выделяет приличный блок памяти, чтобы освободить его при необходимости.
Он вышел из офиса и объявил, что снизил объём памяти до необходимого, и все чествовали его как героя проекта.
Как ни был я тогда шокирован таким «варварским» подходом, должен признать, что сейчас полностью поддерживаю его. Пока я не дошёл до такого образа
— Ноэль Льопис (Noel Llopis)
Автор: PatientZero