Недавнее обсуждение опасности дверей в геймдеве напомнило мне о баге, вызванном дверью из игры, о которой вы, возможно слышали — Half Life 2. Усаживайтесь поудобнее, мы начинаем.

A Combine soldier threatening with a baton, in front of a door. Which one is a greater menace in gamedev?
Солдат Альянса, стоя возле двери, угрожает игроку дубинкой. Что из этого представляет бóльшую угрозу в геймдеве?

Когда-то я работал в Valve над проектами виртуальной реальности. Это было в 2013 году, примерно когда появился Oculus DK1. Мы с Джо Людвигом решили, что лучше всего можно понять, как будет работать VR в контексте реальной игры, портировав в неё реальную игру.

Мы выбрали Team Fortress 2 (причина этого — отдельная история, которой я не хочу здесь касаться). В TF2 использовался движок Source 1, и так получилось, что двумя другими играми Valve, тоже построенными на этом движке, были Half Life 2 и Portal 1. Поэтому побочным эффектом стало то, что они тоже будут работать в VR.

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

Зато HL2 игрался достаточно неплохо. Джо потратил довольно много времени на то, чтобы уровни с лодкой работали прилично.

Почти в начале игры есть эпизод с укладыванием коробок одна на другую. В оригинале он довольно бесил, коробки постоянно сваливались, однако в VR складывать их было очень просто.

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

A Half Life 2 "manhack" - a flying drone with a circular saw

К счастью, для повторного выпуска HL2 были и другие причины (см. https://developer.valvesoftware.com/wiki/Source_2013), а VR-версия работала довольно неплохо, поэтому мы добавили поддержку VR в командную строку, назвали это бетой, пересобрали весь HL2, и стали готовиться к выпуску.

Разумеется, к тому времени мы довольно долго играли в HL2, тестируя на работоспособность все VR-элементы. Но мы переходили только к самым важным главам, не проходя игру с самого начала. А я уже давненько не проходил игру, поэтому решил сделать это VR, от начала до конца. Если я обнаружу, что что-то не работает, то хотя бы смогу задокументировать это в release notes.

Итак. я запустил HL2, выбрал новую игру и начал введение. Это известная часть сюжета: мы прибываем на железнодорожную станцию, видим сообщение Брина, охранник заставляет нас поднять банку, а потом нам нужно зайти в комнату... и... тут я застрял. Я не умер, просто никуда не мог двинуться. Я застрял в коридоре с охранником и никуда не мог пойти. Странно.

На самом деле, всё должно происходить так: охранник (спойлер: на самом деле это замаскированный Барни) стучит в дверь, дверь открывается, он приказывает зайти, после чего игра ждёт, пока игрок войдёт в комнату, чтобы продолжить выполнение скрипта.

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

Я поискал в Интернете видео, подумав, что меня подводит память, но нет: дверь должна открыться автоматически, после чего игрок в неё входит.

Но... этого не происходит!

Ой-ёй, в таком виде игру выпускать нельзя. Я собрал людей, в том числе и тех, кто изначально работал над HL2; да, выяснилось, что игра поломана. И она поломана, даже если играть не в VR — причиной поломки не стали наши с Джо действия. Но никто не знал почему так происходит: соответствующий код остался тем же.

Кто-то даже вернулся к истории исходников и скомпилировал оригинал игры в том же виде, в котором её выпустили: нет, оригинальная версия тоже оказалась поломанной. Как такое вообще может быть? Люди начали сходить с ума: это не какой-то обычный баг, он вернулся в прошлое и инфицировал оригинал!

Пока мы примерно день вспоминали, как работать с инструментами отладки и воспроизведения, кто-то умный (к сожалению, не помню, кто) разобрался, что же не так.

Как можно увидеть в видео, когда когда открывается дверь, в��утри комнаты, слева от открывающейся двери, стоит второй охранник. Этот охранник стоит чуть ближе, чем нужно — самый уголок его ограничивающего параллелепипеда пересекается с траекторией двери при открытии. Дверь начинает открываться, немного касается ступни охранника, отталкивается от неё, закрывается и автоматически закрывается. А поскольку скрипта для обработки такой ситуации и повторного открывания двери нет, на этом моменте игрок застревает.

Как только мы разобрались в этом, исправить ошибку стало очень легко — отодвинуть охранника назад примерно на миллиметр. Элементарно. Но чтобы выяснить это, понадобилось много труда, потому что разработчикам пришлось снова осваивать инструменты отладки и тому подобное.

Two Combine soldiers with the problematic door in question. Text says "The right man in the wrong place" and points to the one on the right.
Нужный человек не в том месте

Здорово, теперь мы можем выпустить игру. Но почему она вообще работала? Ступня охранника находилась на пути двери и в оригинале. Как я сказал, мы вернулись назад и скомпилировали оригинал с исходным кодом выпущенной игры, но баг проявился и в нём тоже. Он присутствовал всегда. Почему раньше дверь не запиралась обратно? Как в принципе можно было выпустить игру?

Все эти вопросы заставили нас начать ещё более длительный поиск багов. Ответом на них стали (как это часто бывает в моих историях) старые добрые числа с плавающей запятой. Оригинал Half Life 2 был выпущен в 2004 году, и хотя набор команд SSE уже существовал, он использовался далеко не везде, поэтому основная часть HL2 компилировалась с расчётом на использование более старого набора математических команд 8087, или x87. Точность в нём менялась хаотически — что-то вычислялось в 32 битах, что-то — в 64 битах, некоторые команды были 80-битными, и точность, получаемая в конкретном фрагменте кода, была довольно запутанным вопросом.

Но десять лет спустя, в 2013 году, SSE уже долгое время был стандартом для всех процессоров x86 — операционные системы полагались на его наличие, поэтому и мы могли полагаться тоже. Разумеется, теперь компиляторы использовали его по умолчанию: на самом деле, для генерации старого (чуть более медленного) кода x87 потребовались бы обходные пути. В SSE применяется гораздо более чётко определённая точность (32-битная или 64-битная, в зависимости от того, что требуется коду), поэтому он намного предсказуемее.

Что ж, проблема вроде решена? Из-за 80-битной точности коллизия не происходила, но при 32-битной она случается, значит, чем больше битов, тем лучше? Ну, не совсем.

Ступня охранника пересекается с траекторией двери в обоих случаях — несколько миллиметров всё равно намного больше погрешностей обеих точностей. В версиях и SSE, и x87 дверь ударяет по мизинцу охранника. Пока они в этом согласуются.

Эта коллизия моделируется должным образом — важной инновацией HL2 стало расширенное использование движка реальной физики. И дверь, и охранник — физические объекты, у обоих есть момент, оба придают импульс друг другу, и хотя у петель двери нет трения, ботинки охранника имеют определённую степень трения с полом.

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

Но в версии с SSE вся эта мелкая точность ненамного отличается, и из-за сочетания трения об пол и массы объектов охранник поворачивается от коллизии, но на недостаточную величину.

Комментарии (0)