Хабрахабр

[Перевод] Грязные трюки разработчиков видеоигр

Предыдущие части: раз, два, три.

Благодарим за игру!

В первой части 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 тысяч копий картриджей. Но вскоре стало очевидно, что никто за долгие месяцы не сбрасывал флеш-чипы на тестовых картриджах, чтобы проверить правильность выполнения процедур инициализации флеш-памяти. И никто не заказывал картриджи для тестирования.

image

Только после заказа всех картриджей мы обнаружили, что код инициализации флеш-памяти умер, и картриджи не могут правильно сохранять игры! Вся студия сошла с ума, пытаясь понять, как выпустить 250 тысяч сломанных картриджей. Мы попробовали реализовать рекомендации производителей, добавив дополнительные резисторы и другие хаки, но ничего не помогало.

Поэтому в каждую коробку с картриджем была вложена листовка с описанием того, как использовать эту «фичу». Когда всё казалось потерянным, кто-то выяснил, что если играть в игры в очень странном и чётко заданном порядке, то флеш-память вроде бы начинает работать.

— Крис Кёрби

Задымление

Мой любимый «хак последней минуты» использовали в режиме для четырёх игроков игры Nitrobike для PS2. Как обычно, дизайнеры уровней и художники делали свою работу, ни капли не осознавая, насколько она осуществима в реальном мире, и, как обычно, работу по «завершению» игры целиком оставили мне.

Но это не могло спасти один конкретный уровень с кинематографической тематикой. После множества споров и столкновений я заставил их упростить графику уровней, создать визуально скрываемые области и убрать излишние динамические объекты. Сам дизайн уровня противился сокрытию лишнего — он состоял из двух огромных комнат (по сюжету — звукозаписывающих павильонов), которые ничем не перекрывались и соединялись двумя открытыми дверями.

image

Одна дверь была посередине стены. Не было никакого способа выстроить перекрывающие полигоны для блокирования одной комнаты, чтобы её нельзя было видеть из другой, как и не было способов удалить какую-нибудь графику уровня, не разрушив его стиль. Мне необходимо было найти способ подставить на стену один большой перекрывающий среднюю стену полигон. И потом меня озарило — «стена дыма» из частиц, закрывающая среднюю дверь, хорошо будет сочетаться с уровнем в кинотематике и полностью решит мою проблему.

Я имел возможность взять себе одного художника для создания такого эмиттера и одного дизайнера уровней, чтобы расположить эмиттеры с обеих сторон двери. Стену дыма можно проехать насквозь, но не видеть через неё, и она скроет существование моего перекрывающего полигона!

Это выглядело хорошо и устраняло последнюю проблему со скоростью уровней. И наконец посередине я вставил один очень большой перекрывающий полигон.

— Стивен Босуэлл II

… а может сюда?

Я пишу игры уже более 20 лет, а недавно работал техническим руководителем в THQ, поэтому насмотрелся на всевозможные ужасные хаки. Но есть один, над которым я смеюсь до сих пор. Случился он с Beam Software в начале 1990-х.

Во всех файлах .s содержался соответствующий ассемблерный код для отдельных частей игры: creature1.s, collision.s, controls.s и так далее. В ту эпоху, когда ещё не было удобных IDE и умных компиляторов, мы писали все игры на языке ассемблера. Кроме того, мы использовали makefile — программист создавал новый файл .s и размещал его после всего остального в makefile.

У нас был один программист, печально известный написанием кода с багами, который мешал каким-нибудь случайным областям памяти. Идея заключалась в том, что ты вводишь в командную строку make, и ассемблер собирает каждый файл в новый файл .o, после чего компоновщик соединяет их все вместе для сборки готового исполняемого файла. Обычно это были переполнения буфера.

Это означало, что случайно записываемый фрагмент памяти теперь находился в в каком-то другом месте, но благодаря чистой удаче игра почему-то переставала вываливаться. Он тратил какое-то время на нахождение этих багов, но если ему это не удавалось, то он… изменял порядок файлов в makefile, чтобы файлы статически компоновались в памяти в другом порядке! Он делал так до тех пор, пока практически любые перемены в makefile не начали приводить к сбоям.

Он просто продолжил создавать новые файлы .s, заполненные небольшими фрагментами данных, которые вставлял в произвольные места в makefile, пока они каким-то образом не перестали крашиться, после чего выпустил её! В последний момент, когда игру уже нужно было выпускать, он решил эту проблему. В тот период было по крайней мере две игры (для Game Boy), выпущенные с использованием такой «техники».

— Шейн Стивенс

Подстандарт

Мы стремились выпустить игру World Series of Poker 2008, которая стала нашим первым проектом на PlayStation 3. PS3 поддерживала несколько разных разрешений экрана и два соотношения сторон. Мы создали 2D-оболочку для широкого экрана, но нам не хватило времени и ресурсов на 2D-оболочку со стандартным разрешением. Я тщательно изучил требования разработчика и не нашёл никаких причин, по которым был бы запрещён леттербоксинг.

Издатель отчаянно пытался подогнать требования, которые бы заставили нас это исправить, но потом сдался и мы выпустили игру как есть. Поэтому наш режим со стандартным соотношением был просто широкоэкранным режимом с чёрными полосами над и под картинкой. Сомневаюсь, что многие подключали это чудо техники (PS3) к старым ламповым телевизорам! К тому же, через несколько часов после покупки собственной PS3, поиграв на телевизоре со стандартным соотношением сторон, я пошёл и купил себе широкоэкранный телевизор.

image

— Стивен Босуэлл II

Стремление к константам

По какой-то причине у нас возникла огромная несовместимость между имевшейся кодовой базой и новыми миллиардами строк кода, написанными для того, чтобы использовать старые библиотеки кода. Первый код был написан людьми, которым была близка идея «безопасного» программирования, как можно более строгого и ограничивающего для избежания ошибок. Поэтому они использовали функцию языка C / C++ под названием CONST.

Код с CONST и без CONST оказался несовместимым друг с другом, из-за чего компилятор ругался. CONST означает «константа»; она гарантирует, что переменные только для чтения невозможно изменить внутри функций. Поэтому конструкция const int x; при предварительной обработке перед компиляцией становилась int x;. Команда разработчиков решила вставить в код хак и выполнила хитрый маленький трюк: #define const, чтобы константа была… ничем, пустым местом. Это аналогично тому, что вы купили машину, сняли два колеса и используете её в качестве мотоцикла.

Но некоторые считали такой подход аналогом отрезания ремня безопасности ножницами. Лично я ужасно ненавижу CONST, поэтому мне понравился этот трюк.

— Аноним

Реальность кусается

У нас был баг в игре на движке Unreal Engine 3 для PlayStation 3 (это была первый выпускаемый проект на UE3 для PS3): во режиме отладки игра необъяснимым образом вываливалась во время printf() при подключении к многопользовательской игре.

Мы не смогли придумать хорошее решение проблемы, но #ifndef PS3 работал вполне нормально до следующей сборки данных, в которой баг исчез. Клиент в режиме отладки выводил все хэши каждого из пакета контента, который приказывал ему загружать сервер, и очевидно, в одном из хэшей (в этой сборке) присутствовал %.

Катастрофа произошла уже после выпуска игры, когда мы работали над патчем контента. Примерно год спустя в следующем проекте я натолкнулся на тот же самый баг и воспользовался точно таким же исправлением. Поэтому в условиях плохого соединения функции включения готовности и состояния передачи голоса в лобби иногда не вызывались. Создание DLC/патча выполнялось таким образом, что мы не могли пропатчить скомпилированный UnrealScript, но у нас был баг, при котором два удалённых вызова процедур не были помечены как надёжные; это означало, что пакеты для их вызова будут передаваться заново до получения подтверждения. Однако пометить удалённые вызовы процедур как надёжные можно только UnrealScript, а мы не могли патчить UnrealScript.

Всё работало замечательно. Поэтому при загрузке мы обходили в цикле все загруженные объекты UFunction (которые являются представлением функции скрипта на C++), выполняли строковое сравнение имён и присваивали этим двум вызовам флаг «надёжный».

— Аноним

Ваши данные готовы

Игры Xbox Live Arcade на первом Xbox должны были целиком упаковываться в файл .xex. Чтобы реализовать это, мы хранили все данные в файле .zip, встроенном как раздел данных в исполняемом файле. Постепенно файл так разросся, что мы больше не могли загружать раздел данных в память, выделять достаточно памяти для его распаковки и вытаскивать нужный файл.

Благодаря этому файловый поток, который считывал исполняемый файл, мог просто переходить на смещения разных zip-файлов и выводить их прямо из исполняемого файла без загрузки разделов данных в память. Чтобы устранить проблему, я написал код, который после загрузки игры считывает заголовок PE исполняемого файла и записывает смещения разделов данных.

— Пэт Уилсон

Камера-обскура

Я расскажу о старом случае: Force 21 была одной из первых трёхмерных RTS, в которой использовалась плавающая камера для наблюдения за текущим отрядом. К концу проекта у нас появился странный баг, при котором камера прекращала следовать за отрядом — она просто останавливалась, пока отряд продолжал двигаться, и ничто не могло сдвинуть её с места. Это случалось в случайные моменты времени и мы не могли воспроизвести ошибку.

Благодаря этой информации мне удалось найти источник ошибки. Так продолжалось до тех пор, пока один из тестеров не заметил, что это происходит чаще, когда рядом с техникой игрока происходит авиаудар. Но у него была и ещё одна характеристика: PhysicalObject мог воспринимать урон. Так как камера использовала скорость и ускорение, а также могла участвовать в коллизиях, я сделал её наследуемой от класса PhysicalObject, который обладал такими характеристиками. Авиаудары наносили высокий урон в довольно большом радиусе, поэтому они в буквальном смысле «убивали» камеру.

Думаю, можно уверенно сказать, что в нашей игре была самая мощная на свете камера. Я исправил эту ошибку, сделав так, чтобы камеры не получали урона, но чтобы подстраховаться я повысил значения их брони и энергии до огромных значений.

— Джим Ван Верт

image

Хексапильность

Я занимался тестингом 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 по сравнению с аркадным автоматом начислялись неправильно. Саму проблему я не помню, но там было что-то вроде ситуации, когда большой крест разбивался на несколько диагоналей.

Мы запускали код миллион раз, сравнивали свой ассеблерный код с кодом на C аркадного автомата, и баг был невозможен. Не нужно говорить, что я обнаружил это примерно в 11:30 вечера (прямо перед завершением срока). (Подозреваю, что Дэйв был немного похож на Майка — отлично разбирался в ассемблере и BASIC, но недолюбливал C двадцать лет назад, когда был сделан Klax.) Мы считали очки правильно, и наш код выполнял точно то же и в том же порядке, что и программа на C, которая была просто построчным переносом того, что Дэйв изначально сделал на Amiga BASIC.

image

Наконец примерно в пять утра, потратив на это всю ночь, мы пришли к мысли, которая могла и не заработать, но попробовать её стоило. Майк написал систему подсчёта очков на Quick BASIC, и очки начали считаться точно так же, как и на аркадном автомате. Затем мы построчно перенесли BASIC в ассемблер Z80.

Бог знает, почему, но программа вела себя в точности как на аркадном автомате (возможно, это как-то связано с тем, что оригинал был написан на BASIC). Это сработало. За завтраком мы целый час смотрели на код и всё равно не поняли, почему он работает по-другому. Мы отправили сборку Atari, распечатали обе версии кода и пошли в кафе. Но иногда, когда становится слишком поздно, настаёт пора заняться вуду-программированием! Мы и сегодня можем оба поклясться, что код должен был давать идентичные результаты!

— Крис Чарла

Вот два неигровых примера, которые слишком любопытны, чтобы не включить их в качестве бонуса. Игры — не единственная область, в которой хаки могут спасти ситуацию.

Мойщики окон

Пять лет назад я работал программистом в области разработки ПО для видеонаблюдения. Мы писали очень чувствительное и сложное ПО безопасности. У нас был замечательный, хорошо работающий продукт. Самой сложной частью ПО было одновременное отображение на экране 50 видеопотоков. Для работы ПО требовался огромный объём памяти, и оно должно было функционировать 24/7.

Неделю спустя мы заметили огромную утечку памяти — примерно по 4 КБ в минуту. За несколько недель до выпуска мы передали заказчикам бета-версию. Память была важнейшей частью ПО, и утечка такого размера полностью убила бы приложение. Я потратил пару дней на изучение утечки, но это не дало никаких результатов, и у нас не оставалось времени, чтобы устранить её до выпуска. Проводя тестирование (в Windows), мне приходилось сворачивать окно ПО, чтобы возвращаться к окну с кодом, и во время этого я замечал серьёзное снижение занимаемой памяти.

Это был наш шанс! Потом я вспомнил, что при переносе окна в область уведомлений или в панель меню «Пуск» Windows мгновенно восстанавливает неиспользуемую/освобождённую память. На экране это выглядело как мерцание, зато работало! Я добавил в приложение таймер, который каждые пару минут перемещал окно в панель меню «Пуск», а потом сразу же разворачивал его во весь экран. После этого мы смогли выпустить приложение, что дало нам ещё немного времени на исправление бага (который мы обнаружили через несколько дней — какой-то дескриптор окна не выполнял очистку должным образом).

— Йохан Лауни

Результаты могут отличаться

В 1970-х я с командой работал над банковской системой. Мы писали на давно забытом языке программирования MPL2. В этом языке было ограничение в 256 глобальных переменных, и поскольку все они использовались, для добавления новых функцию в систему часто приходилось искать переменную, которую можно освободить или использовать для двух разных целей в разных частях кода.

Внутри программы каждая функция могла иметь 256 собственных переменных, ограниченных областью видимости функции. Это был рискованный и долгий процесс. На следующее утро я предложил начальнику обернуть весь код в функцию. Однажды, когда я был дома, на меня снизошло озарение. Тогда у нас бы появилось для программы 256 глобальных переменных, потому что используемые пока 256 будут надёжно храниться во «внутренней» функции.

Но теперь бы она могла «видеть» не только собственные 256 переменных, но и новые 256 глобальных переменных. Сама программа просто объявляла бы ещё несколько переменных, а затем вызывала бы функцию, в которой находилась бы вся исходная программа. Мы смогли обойти проблему, которая стояла перед командой в течение пары лет. Мой босс был настроен скептически, но выделил мне двухчасовое окно времени компиляции и был ошеломлён тем, что это сработало.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть