Предыдущие части: раз, два, три.
Благодарим за игру!
В первой части Wing Commander при выходе из игры мы получали от нашего менеджера памяти EMM386 исключение. Экран очищался и на него выводилась единственная строка, что-то типа «Ошибка менеджера памяти EMM386. Бла-бла-бла».
Нам нужно было выпустить игру как можно быстрее, поэтому я отредактировал ошибку менеджера памяти в hex-редакторе, чтобы она выглядела как «Благодарим за то, что играли в Wing Commander».
— Кен Демарест
Стопроцентно чистые фруктовые соки
Когда я впервые начал работать в игровой индустрии, бОльшую часть времени я трудился в разных небольших скупо финансируемых стартапах. Вот страшилка из тех времён, когда мужчины были мужчинами и использовали DirectX 7. Я работал в компании, которую издатель заставил использовать конкретный 3D-движок. Я не буду его называть, но издатель утверждал, что купил кучу лицензий на него в крупной сделке с лицензированием, поэтому настаивал на его использовании.
Скажем прямо — движок не работал, и бОльшую часть времени работы в компании я занимался тем, что учил 3D-движок правильно делать очевидные вещи, такие как реализация однопроходного наложения карт освещения из нескольких текстур.
Одной из самых интересных неработавших вещей был BSP-компилятор. Дизайнеры уровней создавали геометрию уровней с правильной видимостью, после чего незначительные изменения геометрии нарушали области видимости совсем в другой части карты. Я и по сей день не знаю, почему так происходило, но полагаю, что BSP-компилятор движка добавлял кисти в BSP-дерево в случайном порядке и определённые комбинации… просто произвольным образом всё ломали.
В то время я ещё не слышал о рандомизированном алгоритме, но тем не менее изобрёл его — к BSP-компилятору мы добавили этап предварительной обработки, который перемешивал порядок кистей перед их передачей BSP-компилятору. Если геометрия уровня ломалась в BSP-компиляторе, мы просто пробовали тасовать кисти с другими случайными числами, пока не находили работающую комбинацию и придерживались её, пока BSP-компилятор не ломался в очередной раз.
Сама игра была катастрофой — и движок, и игра попали в комикс Penny Arcade, в котором впервые появился Fruitf*cker 2000. Это остаётся важным пунктом моей карьеры.
— Николас Вининг
Перемотка вперёд
Я работал над NBA JAM TE для Sega Genesis, в которой использовался флеш-чип для сохранения игровых данных. Игру тестировали несколько месяцев, и уже всё было готово к выпуску, поэтому издатель заказал 250 тысяч копий картриджей. Но вскоре стало очевидно, что никто за долгие месяцы не сбрасывал флеш-чипы на тестовых картриджах, чтобы проверить правильность выполнения процедур инициализации флеш-памяти. И никто не заказывал картриджи для тестирования.
Только после заказа всех картриджей мы обнаружили, что код инициализации флеш-памяти умер, и картриджи не могут правильно сохранять игры! Вся студия сошла с ума, пытаясь понять, как выпустить 250 тысяч сломанных картриджей. Мы попробовали реализовать рекомендации производителей, добавив дополнительные резисторы и другие хаки, но ничего не помогало.
Когда всё казалось потерянным, кто-то выяснил, что если играть в игры в очень странном и чётко заданном порядке, то флеш-память вроде бы начинает работать. Поэтому в каждую коробку с картриджем была вложена листовка с описанием того, как использовать эту «фичу».
— Крис Кёрби
Задымление
Мой любимый «хак последней минуты» использовали в режиме для четырёх игроков игры Nitrobike для PS2. Как обычно, дизайнеры уровней и художники делали свою работу, ни капли не осознавая, насколько она осуществима в реальном мире, и, как обычно, работу по «завершению» игры целиком оставили мне.
После множества споров и столкновений я заставил их упростить графику уровней, создать визуально скрываемые области и убрать излишние динамические объекты. Но это не могло спасти один конкретный уровень с кинематографической тематикой. Сам дизайн уровня противился сокрытию лишнего — он состоял из двух огромных комнат (по сюжету — звукозаписывающих павильонов), которые ничем не перекрывались и соединялись двумя открытыми дверями.
Одна дверь была посередине стены. Не было никакого способа выстроить перекрывающие полигоны для блокирования одной комнаты, чтобы её нельзя было видеть из другой, как и не было способов удалить какую-нибудь графику уровня, не разрушив его стиль. Мне необходимо было найти способ подставить на стену один большой перекрывающий среднюю стену полигон. И потом меня озарило — «стена дыма» из частиц, закрывающая среднюю дверь, хорошо будет сочетаться с уровнем в кинотематике и полностью решит мою проблему.
Стену дыма можно проехать насквозь, но не видеть через неё, и она скроет существование моего перекрывающего полигона! Я имел возможность взять себе одного художника для создания такого эмиттера и одного дизайнера уровней, чтобы расположить эмиттеры с обеих сторон двери.
И наконец посередине я вставил один очень большой перекрывающий полигон. Это выглядело хорошо и устраняло последнюю проблему со скоростью уровней.
— Стивен Босуэлл II
… а может сюда?
Я пишу игры уже более 20 лет, а недавно работал техническим руководителем в THQ, поэтому насмотрелся на всевозможные ужасные хаки. Но есть один, над которым я смеюсь до сих пор. Случился он с Beam Software в начале 1990-х.
В ту эпоху, когда ещё не было удобных IDE и умных компиляторов, мы писали все игры на языке ассемблера. Во всех файлах .s содержался соответствующий ассемблерный код для отдельных частей игры: creature1.s, collision.s, controls.s и так далее. Кроме того, мы использовали makefile — программист создавал новый файл .s и размещал его после всего остального в makefile.
Идея заключалась в том, что ты вводишь в командную строку make
, и ассемблер собирает каждый файл в новый файл .o, после чего компоновщик соединяет их все вместе для сборки готового исполняемого файла. У нас был один программист, печально известный написанием кода с багами, который мешал каким-нибудь случайным областям памяти. Обычно это были переполнения буфера.
Он тратил какое-то время на нахождение этих багов, но если ему это не удавалось, то он… изменял порядок файлов в makefile, чтобы файлы статически компоновались в памяти в другом порядке! Это означало, что случайно записываемый фрагмент памяти теперь находился в в каком-то другом месте, но благодаря чистой удаче игра почему-то переставала вываливаться. Он делал так до тех пор, пока практически любые перемены в makefile не начали приводить к сбоям.
В последний момент, когда игру уже нужно было выпускать, он решил эту проблему. Он просто продолжил создавать новые файлы .s, заполненные небольшими фрагментами данных, которые вставлял в произвольные места в makefile, пока они каким-то образом не перестали крашиться, после чего выпустил её! В тот период было по крайней мере две игры (для Game Boy), выпущенные с использованием такой «техники».
— Шейн Стивенс
Подстандарт
Мы стремились выпустить игру World Series of Poker 2008, которая стала нашим первым проектом на PlayStation 3. PS3 поддерживала несколько разных разрешений экрана и два соотношения сторон. Мы создали 2D-оболочку для широкого экрана, но нам не хватило времени и ресурсов на 2D-оболочку со стандартным разрешением. Я тщательно изучил требования разработчика и не нашёл никаких причин, по которым был бы запрещён леттербоксинг.
Поэтому наш режим со стандартным соотношением был просто широкоэкранным режимом с чёрными полосами над и под картинкой. Издатель отчаянно пытался подогнать требования, которые бы заставили нас это исправить, но потом сдался и мы выпустили игру как есть. К тому же, через несколько часов после покупки собственной PS3, поиграв на телевизоре со стандартным соотношением сторон, я пошёл и купил себе широкоэкранный телевизор. Сомневаюсь, что многие подключали это чудо техники (PS3) к старым ламповым телевизорам!
— Стивен Босуэлл II
Стремление к константам
По какой-то причине у нас возникла огромная несовместимость между имевшейся кодовой базой и новыми миллиардами строк кода, написанными для того, чтобы использовать старые библиотеки кода. Первый код был написан людьми, которым была близка идея «безопасного» программирования, как можно более строгого и ограничивающего для избежания ошибок. Поэтому они использовали функцию языка C / C++ под названием CONST.
CONST означает «константа»; она гарантирует, что переменные только для чтения невозможно изменить внутри функций. Код с CONST и без CONST оказался несовместимым друг с другом, из-за чего компилятор ругался. Команда разработчиков решила вставить в код хак и выполнила хитрый маленький трюк: #define const, чтобы константа была… ничем, пустым местом. Поэтому конструкция const int x;
при предварительной обработке перед компиляцией становилась int x;
. Это аналогично тому, что вы купили машину, сняли два колеса и используете её в качестве мотоцикла.
Лично я ужасно ненавижу CONST, поэтому мне понравился этот трюк. Но некоторые считали такой подход аналогом отрезания ремня безопасности ножницами.
— Аноним
Реальность кусается
У нас был баг в игре на движке Unreal Engine 3 для PlayStation 3 (это была первый выпускаемый проект на UE3 для PS3): во режиме отладки игра необъяснимым образом вываливалась во время printf() при подключении к многопользовательской игре.
Клиент в режиме отладки выводил все хэши каждого из пакета контента, который приказывал ему загружать сервер, и очевидно, в одном из хэшей (в этой сборке) присутствовал %. Мы не смогли придумать хорошее решение проблемы, но #ifndef PS3 работал вполне нормально до следующей сборки данных, в которой баг исчез.
Примерно год спустя в следующем проекте я натолкнулся на тот же самый баг и воспользовался точно таким же исправлением. Катастрофа произошла уже после выпуска игры, когда мы работали над патчем контента. Создание DLC/патча выполнялось таким образом, что мы не могли пропатчить скомпилированный UnrealScript, но у нас был баг, при котором два удалённых вызова процедур не были помечены как надёжные; это означало, что пакеты для их вызова будут передаваться заново до получения подтверждения. Поэтому в условиях плохого соединения функции включения готовности и состояния передачи голоса в лобби иногда не вызывались. Однако пометить удалённые вызовы процедур как надёжные можно только UnrealScript, а мы не могли патчить UnrealScript.
Поэтому при загрузке мы обходили в цикле все загруженные объекты UFunction (которые являются представлением функции скрипта на C++), выполняли строковое сравнение имён и присваивали этим двум вызовам флаг «надёжный». Всё работало замечательно.
— Аноним
Ваши данные готовы
Игры Xbox Live Arcade на первом Xbox должны были целиком упаковываться в файл .xex. Чтобы реализовать это, мы хранили все данные в файле .zip, встроенном как раздел данных в исполняемом файле. Постепенно файл так разросся, что мы больше не могли загружать раздел данных в память, выделять достаточно памяти для его распаковки и вытаскивать нужный файл.
Чтобы устранить проблему, я написал код, который после загрузки игры считывает заголовок PE исполняемого файла и записывает смещения разделов данных. Благодаря этому файловый поток, который считывал исполняемый файл, мог просто переходить на смещения разных zip-файлов и выводить их прямо из исполняемого файла без загрузки разделов данных в память.
— Пэт Уилсон
Камера-обскура
Я расскажу о старом случае: Force 21 была одной из первых трёхмерных RTS, в которой использовалась плавающая камера для наблюдения за текущим отрядом. К концу проекта у нас появился странный баг, при котором камера прекращала следовать за отрядом — она просто останавливалась, пока отряд продолжал двигаться, и ничто не могло сдвинуть её с места. Это случалось в случайные моменты времени и мы не могли воспроизвести ошибку.
Так продолжалось до тех пор, пока один из тестеров не заметил, что это происходит чаще, когда рядом с техникой игрока происходит авиаудар. Благодаря этой информации мне удалось найти источник ошибки. Так как камера использовала скорость и ускорение, а также могла участвовать в коллизиях, я сделал её наследуемой от класса PhysicalObject, который обладал такими характеристиками. Но у него была и ещё одна характеристика: PhysicalObject мог воспринимать урон. Авиаудары наносили высокий урон в довольно большом радиусе, поэтому они в буквальном смысле «убивали» камеру.
Я исправил эту ошибку, сделав так, чтобы камеры не получали урона, но чтобы подстраховаться я повысил значения их брони и энергии до огромных значений. Думаю, можно уверенно сказать, что в нашей игре была самая мощная на свете камера.
— Джим Ван Верт
Хексапильность
Я занимался тестингом The New Tetris для N64. У нас случался сбой, который я мог воспроизвести в любой момент: на экран выводился дамп регистров, после чего игра зависала. Чтобы избавиться от зависания, приходилось отключать и включать питание N64: даже клавиша сброса не реагировала.
Версия за версией разработчик говорил, что баг исправлен, и версия за версией я его воспроизводил. Приближался дедлайн выпуска, и чтобы выпустить игру, разработчику необходимо было устранить все приводящие к сбоям баги. (Nintendo самостоятельно выполняла тестирование даже игр других компаний, и для выпуска игры Nintendo должна была её одобрить.) Но от этого бага никак не могли избавиться.
Также в игре были никак не связанные с багом секретные коды, которые можно было вводить для разблокирования разных возможностей. Однажды я пошутил, что разработчику стоит заменить экран шестнадцатеричного дампа надписью «Поздравляем! Вы обнаружили секретный код! Отключите и снова включите консоль, а потом введите имя пользователя HALUCI». А он так и сделал. Именно благодаря этому игра была выпущена.
— Аноним
Короткий стек
В 1982-83 годах я был одним из нескольких интернов в IMAGIC, и в то время все мы делали картриджи для Intellivision. Одному из программистов нужно было вернуться к учёбе, поэтому меня выбрали, чтобы я устранил баг с зависанием в его игре. Это оказалось переполнение стека в обработчике таймерного прерывания.
Так как единственной задачей обработчика было обновление отображения экранного таймера, я добавил в начале процедуры прерывания код для тестирования глубины стека. Если существовала опасность переполнения стека, то процедура выполняла возврат, ничего не делая. Так как обработчик вызывался много раз в секунду, игрок ничего не замечал, а ошибка была исправлена.
— Аноним
Назад к основам
Мы с коллегой Майком Мика портировали игру о собирании тайлов Klax с аркадных автоматов на Game Boy Color. Это был интересный, напряжённый шестинедельный проект по переносу на систему одной из наших любимейших игр.
У нас был исходный код на C (на самом деле от игры Escape from the Planet of the Robot Monsters, только большинство роботом-монстров закомментировано и заменено на код Klax), и мы часто общались с программистом оригинальной аркадной игры Дэйвом Экерсом, который написал за выходные прототип на Amiga BASIC и портировал его на C примерно за день.
Мы кодировали игру на ассемблере Z80. Было много интересного, например, мы переписывали код на белую доску и мысленно проходили строку за строкой, обновляя содержимое памяти на другой белой доске, потому что у нас не было настоящего отладчика. Хорошие были времена.
Сроки уже поджимали, но всё работало нормально. Я играл в аркадную версию, тестировал версию для GBC и обнаружил странный баг подсчёта очков. Саму проблему я не помню, но там было что-то вроде ситуации, когда большой крест разбивался на несколько диагоналей. Как бы то ни было, очки на GBC по сравнению с аркадным автоматом начислялись неправильно.
Не нужно говорить, что я обнаружил это примерно в 11:30 вечера (прямо перед завершением срока). Мы запускали код миллион раз, сравнивали свой ассеблерный код с кодом на C аркадного автомата, и баг был невозможен. Мы считали очки правильно, и наш код выполнял точно то же и в том же порядке, что и программа на C, которая была просто построчным переносом того, что Дэйв изначально сделал на Amiga BASIC. (Подозреваю, что Дэйв был немного похож на Майка — отлично разбирался в ассемблере и BASIC, но недолюбливал C двадцать лет назад, когда был сделан Klax.)
Наконец примерно в пять утра, потратив на это всю ночь, мы пришли к мысли, которая могла и не заработать, но попробовать её стоило. Майк написал систему подсчёта очков на Quick BASIC, и очки начали считаться точно так же, как и на аркадном автомате. Затем мы построчно перенесли BASIC в ассемблер Z80.
Это сработало. Бог знает, почему, но программа вела себя в точности как на аркадном автомате (возможно, это как-то связано с тем, что оригинал был написан на BASIC). Мы отправили сборку Atari, распечатали обе версии кода и пошли в кафе. За завтраком мы целый час смотрели на код и всё равно не поняли, почему он работает по-другому. Мы и сегодня можем оба поклясться, что код должен был давать идентичные результаты! Но иногда, когда становится слишком поздно, настаёт пора заняться вуду-программированием!
— Крис Чарла
Игры — не единственная область, в которой хаки могут спасти ситуацию. Вот два неигровых примера, которые слишком любопытны, чтобы не включить их в качестве бонуса.
Мойщики окон
Пять лет назад я работал программистом в области разработки ПО для видеонаблюдения. Мы писали очень чувствительное и сложное ПО безопасности. У нас был замечательный, хорошо работающий продукт. Самой сложной частью ПО было одновременное отображение на экране 50 видеопотоков. Для работы ПО требовался огромный объём памяти, и оно должно было функционировать 24/7.
За несколько недель до выпуска мы передали заказчикам бета-версию. Неделю спустя мы заметили огромную утечку памяти — примерно по 4 КБ в минуту. Я потратил пару дней на изучение утечки, но это не дало никаких результатов, и у нас не оставалось времени, чтобы устранить её до выпуска. Память была важнейшей частью ПО, и утечка такого размера полностью убила бы приложение. Проводя тестирование (в Windows), мне приходилось сворачивать окно ПО, чтобы возвращаться к окну с кодом, и во время этого я замечал серьёзное снижение занимаемой памяти.
Потом я вспомнил, что при переносе окна в область уведомлений или в панель меню «Пуск» Windows мгновенно восстанавливает неиспользуемую/освобождённую память. Это был наш шанс! Я добавил в приложение таймер, который каждые пару минут перемещал окно в панель меню «Пуск», а потом сразу же разворачивал его во весь экран. На экране это выглядело как мерцание, зато работало! После этого мы смогли выпустить приложение, что дало нам ещё немного времени на исправление бага (который мы обнаружили через несколько дней — какой-то дескриптор окна не выполнял очистку должным образом).
— Йохан Лауни
Результаты могут отличаться
В 1970-х я с командой работал над банковской системой. Мы писали на давно забытом языке программирования MPL2. В этом языке было ограничение в 256 глобальных переменных, и поскольку все они использовались, для добавления новых функцию в систему часто приходилось искать переменную, которую можно освободить или использовать для двух разных целей в разных частях кода.
Это был рискованный и долгий процесс. Внутри программы каждая функция могла иметь 256 собственных переменных, ограниченных областью видимости функции. Однажды, когда я был дома, на меня снизошло озарение. На следующее утро я предложил начальнику обернуть весь код в функцию. Тогда у нас бы появилось для программы 256 глобальных переменных, потому что используемые пока 256 будут надёжно храниться во «внутренней» функции.
Сама программа просто объявляла бы ещё несколько переменных, а затем вызывала бы функцию, в которой находилась бы вся исходная программа. Но теперь бы она могла «видеть» не только собственные 256 переменных, но и новые 256 глобальных переменных. Мой босс был настроен скептически, но выделил мне двухчасовое окно времени компиляции и был ошеломлён тем, что это сработало. Мы смогли обойти проблему, которая стояла перед командой в течение пары лет.
Автор: PatientZero