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

Когда-то я работал в Valve над проектами виртуальной реальности. Это было в 2013 году, примерно когда появился Oculus DK1. Мы с Джо Людвигом решили, что лучше всего можно понять, как будет работать VR в контексте реальной игры, портировав в неё реальную игру.
Мы выбрали Team Fortress 2 (причина этого — отдельная история, которой я не хочу здесь касаться). В TF2 использовался движок Source 1, и так получилось, что двумя другими играми Valve, тоже построенными на этом движке, были Half Life 2 и Portal 1. Поэтому побочным эффектом стало то, что они тоже будут работать в VR.
Точнее, Portal 1 «работал», однако все трюки с перспективой при прохождении через портал вызывали настоящую тошноту, поэтому играть в это было практически невозможно.
Зато HL2 игрался достаточно неплохо. Джо потратил довольно много времени на то, чтобы уровни с лодкой работали прилично.
Почти в начале игры есть эпизод с укладыванием коробок одна на другую. В оригинале он довольно бесил, коробки постоянно сваливались, однако в VR складывать их было очень просто.
Кроме того, уничтожение мэнхэков монтировкой, на плоских экранах выглядевшее, как паническое размахивание, в VR превращалось в изящный точный удар.

К счастью, для повторного выпуска HL2 были и другие причины (см. https://developer.valvesoftware.com/wiki/Source_2013), а VR-версия работала довольно неплохо, поэтому мы добавили поддержку VR в командную строку, назвали это бетой, пересобрали весь HL2, и стали готовиться к выпуску.
Разумеется, к тому времени мы довольно долго играли в HL2, тестируя на работоспособность все VR-элементы. Но мы переходили только к самым важным главам, не проходя игру с самого начала. А я уже давненько не проходил игру, поэтому решил сделать это VR, от начала до конца. Если я обнаружу, что что-то не работает, то хотя бы смогу задокументировать это в release notes.
Итак. я запустил HL2, выбрал новую игру и начал введение. Это известная часть сюжета: мы прибываем на железнодорожную станцию, видим сообщение Брина, охранник заставляет нас поднять банку, а потом нам нужно зайти в комнату... и... тут я застрял. Я не умер, просто никуда не мог двинуться. Я застрял в коридоре с охранником и никуда не мог пойти. Странно.
На самом деле, всё должно происходить так: охранник (спойлер: на самом деле это замаскированный Барни) стучит в дверь, дверь открывается, он приказывает зайти, после чего игра ждёт, пока игрок войдёт в комнату, чтобы продолжить выполнение скрипта.
Но в нашем случае дверь шумела, но не открывалась, а затем снова закрывалась, поэтому в комнату попасть было невозможно, а ворота позади закрывались, поэтому больше ничего сделать было нельзя. Охранник ждёт бесконечно, указывая на закрытую дверь, и дальше игрок идти не может.
Я поискал в Интернете видео, подумав, что меня подводит память, но нет: дверь должна открыться автоматически, после чего игрок в неё входит.
Но... этого не происходит!

Ой-ёй, в таком виде игру выпускать нельзя. Я собрал людей, в том числе и тех, кто изначально работал над HL2; да, выяснилось, что игра поломана. И она поломана, даже если играть не в VR — причиной поломки не стали наши с Джо действия. Но никто не знал почему так происходит: соответствующий код остался тем же.
Кто-то даже вернулся к истории исходников и скомпилировал оригинал игры в том же виде, в котором её выпустили: нет, оригинальная версия тоже оказалась поломанной. Как такое вообще может быть? Люди начали сходить с ума: это не какой-то обычный баг, он вернулся в прошлое и инфицировал оригинал!
Пока мы примерно день вспоминали, как работать с инструментами отладки и воспроизведения, кто-то умный (к сожалению, не помню, кто) разобрался, что же не так.
Как можно увидеть в видео, когда когда открывается дверь, в��утри комнаты, слева от открывающейся двери, стоит второй охранник. Этот охранник стоит чуть ближе, чем нужно — самый уголок его ограничивающего параллелепипеда пересекается с траекторией двери при открытии. Дверь начинает открываться, немного касается ступни охранника, отталкивается от неё, закрывается и автоматически закрывается. А поскольку скрипта для обработки такой ситуации и повторного открывания двери нет, на этом моменте игрок застревает.
Как только мы разобрались в этом, исправить ошибку стало очень легко — отодвинуть охранника назад примерно на миллиметр. Элементарно. Но чтобы выяснить это, понадобилось много труда, потому что разработчикам пришлось снова осваивать инструменты отладки и тому подобное.

Здорово, теперь мы можем выпустить игру. Но почему она вообще работала? Ступня охранника находилась на пути двери и в оригинале. Как я сказал, мы вернулись назад и скомпилировали оригинал с исходным кодом выпущенной игры, но баг проявился и в нём тоже. Он присутствовал всегда. Почему раньше дверь не запиралась обратно? Как в принципе можно было выпустить игру?
Все эти вопросы заставили нас начать ещё более длительный поиск багов. Ответом на них стали (как это часто бывает в моих историях) старые добрые числа с плавающей запятой. Оригинал 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 вся эта мелкая точность ненамного отличается, и из-за сочетания трения об пол и массы объектов охранник поворачивается от коллизии, но на недостаточную величину.