Привет, хабр! В его предыдущий В статье я рассказал об интересном баге в старой игрушке, наглядно продемонстрировал явление накопления ошибок округления и просто поделился опытом реверс-инжиниринга.
Я надеялся, что на этом все закончится, но сильно ошибался.
Поэтому под катом я расскажу вам продолжение истории о звере по имени Timebug, о 60 кадрах в секунду и об очень интересных решениях в разработке игр.
Фон
Написав предыдущую часть этой эпопеи вокруг неправильно рассчитанного времени трека, я спонтанно постарался затронуть как можно больше игр.
Мне было лень выполнять дополнительную работу, поэтому я искал симптомы во всех играх NFS, которые у меня были на тот момент. Под мишенью также был Underground 2, но первоначальных симптомов я там не обнаружил:
Как видно по картинке (она, кстати, кликабельна), IGT рассчитывается другим способом, который явно привязан к int, и поэтому никакого накопления ошибок быть не должно.
Я с радостью сообщил об этом в нашем сообществе и планировал забыть об этом, но нет: Эвил вручную пересчитал некоторые видео и снова обнаружил разницу во времени.
Мы решили, что пока нет времени этим заниматься и я сконцентрировался на уже известной на тот момент проблеме, но теперь смог выделить время для себя и поработать именно над этой игрой.
Симптомы
Я изучил поведение глобального таймера и обнаружил, что он «сбрасывается» при каждом перезапуске гонки, при каждом выходе в меню и вообще в любой удобный момент. Это не входило в мои планы, поскольку связь с уже известной проблемой была полностью потеряна, и этот таймер просто не должен был сломаться.От отчаяния я записал 10 кругов и посчитал время руками.
К моему удивлению, время круга было на 100% точным.
Интересные факты На самом деле был обнаружен еще один таймер, который тоже считал время в режиме float, но он оказался немного бесполезным.
Меняя его, я не добился видимых результатов.
И почему-то этот таймер int не сбрасывается в 0, а устанавливается на 4000. Оставалось только разобрать и посмотреть в чем дело.
Не вдаваясь в подробности, покажу псевдокод процедуры, считающей этот несчастный ИГТ:
Во-первых:if ( g_fFrameLength != 0.0 ) { float v0 = g_fFrameDiff + g_fFrameLength; int v1 = FltToDword(v0); g_dwUnknown0 += v1; g_dwUnknown1 = v1; g_dwUnknown2 = g_dwUnknown0; g_fFrameDiff = v0 - v1 * 0.016666668; g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5); LODWORD(g_fFrameLength) = 0; ++g_dwFrameCount; g_fIGT = (double)g_dwIGT * 0.00025000001; // Divides IGT by 4000 to get time in seconds }
g_dwIGT += FltToDword(g_fFrameLength * 4000.0 + 0.5);
Изначально этот код показался мне совершенно бессмысленным.
Зачем умножать это на 4000, а потом прибавлять половину? На самом деле это очень хитрая магия.
4000 — это просто константа, пришедшая в голову одному из разработчиков.
А вот +0,5 — это такой интересный способ округления по законам математики.
Прибавьте половину к 4,7, и когда вы сократите его до целого числа, вы получите 5, а когда вы прибавите и округлите 4,3, вы получите 4, как вы и хотели.
Метод не самый точный, но, наверное, работает быстрее.
Лично я возьму на заметку.
А теперь, дорогие читатели, я хочу сыграть с вами в игру.
Посмотрите на полный псевдокод выше и попытайтесь найти там ошибку.
Если вы устали или вам просто не интересно, переходите к следующей части.
Ошибка
Линия g_fFrameDiff = v0 — (double)v1 * 0,016666668; Ошибка находится под спойлером, чтобы случайно не заглянуть.Позвольте мне немного объяснить: 0,01(6) — это 1/60 секунды.
Весь приведенный выше код выглядит как попытка рассчитать и компенсировать заикание движка, но они не учли тот факт, что не все играют со скоростью 60 кадров в секунду.
Вот отсюда и интересный результат, когда у меня на видео все кружочки совпали с реальностью, а у Эвила нет. Он играет с выключенной вертикальной синхронизацией, а игра залочена на максимум 120 фпс и соответственно на его компьютере код работал некорректно.
Я немного модифицировал код выше и привел его в человеческий вид: if ( g_fFrameLength != 0.0 )
{
float tmpDiff = g_fFrameDiff + g_fFrameLength;
int diffTime = FltToDword(v0);
g_dwUnknown0 += diffTime; // Some unknown vars
g_dwUnknown1 = diffTime;
g_dwUnknown2 = g_dwUnknown0;
g_fFrameDiff = tmpDiff - diffTime * 1.0/60;
g_dwIGT += FltToDword(g_fFrameLength * 4000 + 0.5);
g_fFrameLength = 0;
++g_dwFrameCount;
g_fIGT = (float)g_dwIGT / 4000; // Divides IGT by 4000 to get time in seconds
}
Здесь вы можете видеть, что при расчете задержки изначально используется фактическое время кадра, а затем жестко запрограммированные 60 кадров в секунду.
ПОДОЗРИТЕЛЬНЫЙ ! Выключаю vsync и получаю 120 кадров в секунду.
Я иду записывать видео и получаю разницу примерно в 0,3 секунды за круг.
Бинго! Осталось только прошить хардкорные 60фпс на хардкорные 120фпс.
Для этого посмотрите ассемблерный код и найдите адрес, где находится эта волшебная константа: 0x007875BC. Спойлер На самом деле известно, что эта константа имеет тип float/double и будет загружена в FPU. FPU не умеет загружаться из регистра, поэтому ему суждено было оказаться где-то в памяти.
Хорошо, что его не было в стеке, иначе я бы так легко не отделался.
Пришлось бы вносить изменения в сам код игры, чего делать не хотелось.
В этот раз я не писал никаких специальных программ, а просто изменил руками значение этой переменной в Cheat Engine на необходимое значение.
После этого я снова записал 10 кругов и посчитал время — IGT и RTA наконец совпали.
На самом деле они не совпадали на 100%.
Но в основном из-за того, что запись видео сильно снизила у меня частоту кадров, из-за чего игра перестала адекватно рассчитывать разницу во времени.
Но в целом разница составила около 0,02 секунды.
Я еще немного заглянул в код, чтобы узнать, на что влияют те переменные, которые так старательно рассчитывались в процедуре расчета времени.
Я особо не нашел, но где-то в движке рядом с g_fFrameTime используется g_fDiffTime. Скорее всего, мое предположение о компенсации заикания оказалось верным.
Но кто знает этих разработчиков?
Послесловие
Я даже не знаю, сколько раз я сталкивался с игрой, требующей 60 кадров в секунду.Это очень плохое написание игры, и я настоятельно рекомендую вам, читателям, принять во внимание различия в оборудовании.
Особенно, если вы инди-разработчик.
И особенно если вы разрабатываете на ПК.
Для консолей добиться разной аппаратной мощности не получится, а вот на ПК с этим постоянно возникают проблемы.
А еще есть мониторы с частотой обновления 120/144 Гц и даже больше.
И g-sync уже приехал.
Но NFS - это порты с консолей, поэтому в решениях часто бывает чисто консольный подход: считать, что FPS не поднимется выше 60 (30, 25, любое число), а многие решения жестко заточены именно под это количество кадров в секунду.
.
К сожалению, в новых частях серии это стало более заметно.
На этот раз статья получилась не такой объемной, хотя простор для исследований есть.
Надеюсь, в этих играх будет еще больше интересного, о чем можно поговорить.
Теги: #обратное проектирование #обратное проектирование #ошибка #обратное проектирование
-
Почему Люди Любят 3D-Печать
19 Oct, 24 -
Свободный Дизайн (Не) Меняющий Парадигмы
19 Oct, 24 -
Новый Эксплойт Угрожает Iphone
19 Oct, 24