Pull to refresh

Comments 39

Проблема была не во float, а в накоплении ошибок округления. При правильном решении (получать системную отметку времени и вычитать начальное значение) точности float вполне хватило бы.

Думаю, это была «одна из ошибок». В условиях того кода, что есть, банальное использование double полностью исключило бы баг. Я не думаю, что кто-то в здравом уме будет настолько сильно увлечен беспрерывным переигрыванием одного уровня, чтобы дабл «поехал». Хотя я согласен с вашим решением, что и написал в конце статейки :)

Подход "всюду используем только double" жрет много памяти и работает медленнее. А для того чтобы догадаться использовать double для таймера — надо знать про эту ошибку. Но тогда ничего не мешало бы и нормальное решение сделать.

Почему медленней на современных х86? FPU — 80 бит, то есть long double, ширина шины памяти — 64 бита и выше. С чего float будет медленней? Скорее уж наоборот. Ну разве что если кэша не хватит…

Аналогично short int и char работают чуть медленней, чем intна современных машинах.
1 регистр SSE — 2 double или 4 float.
Это касается только тех мест, где векторизованные вычисления вообще возможны. В описанном же примере нет никаких массивов чисел и однообразных операций над ними.

В любой 3D-игре таких мест должно быть полно. Чтобы знать что конкретно в этом месте double бы не помешал — нужно думать. А программист не думал.

В данном случае лучше было использовать целочисленную переменную для внутриигровых тиков. В более старом NFS3, например, внутриигровое время представлено целым числом тиков, где 128 тиков = 1 секунда.
Очень сомневаюсь, насчет «любой», Фиксированная точка (int c масштабированием) быстрее плавающей, поэтому AFAIK старые движки жили на ней.

Кроме того, движок и сама игра — чуть разные вещи и программируются зачастую разными людьми. А в самой игре (без движка) как-то не видно, где векторизация возможна.
Смотря какой процессор.
Гм… ну приведите пример процессора, где умножение и деление с плавающей точкой быстрее, чем целое умножение и деление.
Сейчас многие современные компиляторы отказываются от 80-bit fpu, потому что оно неудобно и тянет старые инструкции.
Всё считают с помощью SSE.
Не факт что это правильное решение. Время в игре может не совпадать с реальным временем по разным причинам: игру поставили на паузу, игра показывает повтор ситуации, игра тормозит и не успевает обсчитывает свою механику в реальном времени, время в игре намеренно ускоряется или замедляется и т.д.

Все эти ситуации решаемые если вспомнить математику.

Заводим целочисленную переменную int32, каждый игровой «тик» добавляем в неё единичку, а затем сразу же в оригинальную переменную пишем значение нашей целочисленной переменной, умноженное на размер тика (то, на сколько в оригинальном коде увеличивается эта переменная каждый раз). Таким образом, накопления ошибки не будет. Это, думаю, можно сделать прямо внутри исполняемого файла, пропатчив соответствующий машинный код. Но нужно, конечно же, детальнее вникнуть в код, чтобы выяснить, подойдёт ли такой вариант :)
затем сразу же в оригинальную переменную пишем значение нашей целочисленной переменной, умноженное на размер тика


Пишем или добавляем?

Вообще, у игры должны быть свои часы, согласно которым происходят все события. Эти часы должны быть целочисельными. Один цикл просчета состояния игры — один игровых тик часов. Потом эти тики пересчитываются в секунды согласно игровых правил.
Пишем. Псевдокод (предположим, игра тикает 120 раз в секунду):
i32_ticks++;
f_step = 1.0 / 120.0
f32_time = i32_ticks * f_step;

В оригинале же, судя по описанию, что-то типа:
f_step = 1.0 / 120.0
f32_time = f32_time + f_step;
А, теперь понял. Мы говорили о том же самом, просто разными словами.
При START_TIME близкому к 300000 секунд количество шагов было почти в два раза меньше, чем ожидалось. При START_TIME, большем магической константы 524288 программа переставала работать.

Если я правильно понимаю, дело не только в накоплении ошибок, но и именно в недостатке точности float.
Если быть совсем честным, то проблема в решении целиком. Оно какое-то неудачное, как мне кажется. Но как уж сделали :)

Если бы ошибки не накапливались — точности float вполне хватило бы.

Я просто хочу сказать, что хотя переменная step и может храниться с достаточной точностью, но когда мы подсчитываем прошедшее время t, step всё равно фактически округляется до того знака, сколько сейчас помещается в частичной сумме. Конечно, вряд ли кто-то будет несколько суток нон-стоп ездить, чтобы всё совсем сломалось, но заметные эффекты, как свидетельствует статья, появляются уже через 15 минут.
Не могу не похвастать тут моим большим патчем для Need For Speed III: Hot Pursuit :) Там даже из машинного кода было ясно, как много не очень качественного кода было написано, который просто должен был сломаться со временем. Целый ряд переполнений значений int32: при определении свободного пространства на диске, количества RAM, количества памяти в GPU, частоты процессора. У игры получались отрицательные числа и она считала, что машина слабее Pentium 166, из-за чего в некоторых случаях запускала ветки кода для самых слабых компьютеров. Также тут было большое количество ошибок вроде переполнения буфера из-за полного отсутствия проверок выхода за пределы массивов и тому подобных глупых ошибок. Например, массив, который хранил в себе список поддерживаемых драйвером видеокарты форматов текстур, был рассчитан на не более чем 14 форматов, и как только драйвера стали поддерживать больше — переполнение буфера и падение. А люди винили в этом новые драйвера (ведь игра стала падать при обновлении драйвера), хотя виноват был именно код в игре. Ну и в этом духе. В сумме уже накопилось наверное сотни две исправлений.
В любой части NFS очень много некачественного кода. Например, ProStreet отказывается нормально работать, если частота процессора не делится на 2 нацело. А совсем правильно он будет работать только на частоте 3.20 ГГц (привет порт с Х360). Хотя трудно в этом винить разработчиков — их заставляли за год клепать ААА игру с поддержкой как минимум 3-х разных платформ (PC, PS2, PS3, X360), я вообще удивлен, что они делали игры стабильные хотя бы на железе уровня выхода игры. Большая часть проблем с ними возникает на более шустрых железках. А мелкие баги не так уж и печальны :)
Было бы интересно прочитать об этом статью!
Простите, что немного не по теме. Хотелось бы узнать, при прохождении таких игр, как NFSU, NFSU2 и вышеупомянутой игры, используются ли стены для поворотов на скорости? Насколько я понимаю, игровая механика каким-то образом учитывает столкновения, которые влияют не только на скорость машинки, но и на скорость ботов? Просто у меня сложилось впечатление, что любой контакт со стеной снижает время прохождения трассы ботами.
Во все игры NFS встроен механизм, который называется Catch-up. Он нацелен на «выравнивание» сложности, чтобы хорошие игроки не скучали, а плохие не сильно страдали от выбранной сложности. Как этот механизм работает доподлинно не известно, но он просто «накручивает» характеристики машинам ИИ и те едут быстрее. Механизм явно меняется от игры к игре. Например в Carbon иногда можно наблюдать, как ИИ в прямом смысле пролетает камеры на скорости 500+ км/ч, что иногда вводит в ступор. Но кетчуп — не односторонний механизм, он так же может накручивать и игроку, но это реже заметно.
Стены используются в основном в NFSU2, в других частях это не дает такого сильного выигрыша. В основном, когда сбросить скорость касанием стены быстрее, чем тормозить, и угол выхода из поворота получается лучше.
Про вашу догадку с сокращением времени и касанием стен: опять же, доподлинно не известно, влияют ли конкретно столкновения, но такая тенденци была замечина. Вот это видео неплохо демонстрирует механику кетчупа в целом и касания стен в частности.
Видимо, команда тестировщиков не успела прокачать свой скилл настолько, что бы видеть такое.
Спасибо за разъяснение, я в основном, то в NFSU2 и играл, проходя несколько раз. Самый последний раз был настолько быстрым, что я даже расстроился, что игра такая короткая. Во время последнего прохождения как раз и было четко заметно влияние столкновений на характеристики ИИ, который, кстати, весьма глуп на поворотах.
Самый суровый catch-up, на момей памяти, был в NFSU. Если стараться удерживать первое место, игра превращалась в ад — боты за спиной проходили участки дороги любой сложности, даже не снижая скорости, гражданские машины «выныривали» в самых неудобных местах. А вот держаться вторым, и рвануть перед финишем было проще всего — тогда и боты начинали ошибаться и врезаться.
Самый суровый был в первой NFS(было проверено при игре на двоих с раздельным экраном на x86). В те далекие времена было замечено, а затем и проверено: чем дальше машина от игрока на первом месте, тем быстрее она движется(хотя показатели на «спидометрах» одинаковые). К примеру, если простоять на месте и дать себя обогнать на пару кругов другому игроку/ботам, то сократить отставание в один круг(догнать игрока на первом месте) при идеальном прохождении всей трассы обоими игроками на одинаковых машинах можно было примерно за половину круга.

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

Ничего не хочу писать умного, просто поделюсь эмоциями:


Хабр — торт! Прочитал статью на одном дыхании.

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

int64 и наносекунды отлично подходят друг к другу. Во многих реализациях c++11 классы std::steady_clock и std::system_clock реализованы именно так.
Идея хороша, если не несколько «но»: в Windows размер тика составляет 1/10000 миллисекунды, а получить «цельное» количество обновлений в секунду очень сложно. В конечном итоге все равно каждый кадр будет занимать 16,(6) миллисекунд, ибо обрубится vsync-ом. Для 120/144Hz мониторов число будет другое, но оно все равно получится не целым, и даже не дробным. Переводя в тики процессора, мы получим 166666,(6) тиков, и при сложении мы будем всякий кадр забывать про ~0,67 тика. Это примерно 40 тиков в секунду. Со временем ошибка накопится и выльется в парочку миллисекунд, что не особо критично, но неприятно.
Можно попытаться выносить обновление физики в отдельный поток и его насильно блокировать на 100фпс, но это уже значительно сложнее реализовать.
В итоге у нас есть 4 разных версии игры, которые надо поддерживать. Этот факт делает «правильный» путь чрезмерно сложным и неоправданным. По-хорошему, нужно слегка подправить запускаемый файл и обнулять счетчик там, но… Править 4 разных экзешника, которые еще и запакованы, да защищены от отладки… Лучше просто напишем простую программку, которая будет в реалтайме отслеживать состояние таймеров и обнулять их при необходимости.

На самом деле нет. Лучше сделать плагин, который загружается вместе с игрой без необходимости запускать какие-либо программы. А для поддержки разных экзешников у нас есть паттерны :)
Пример здесь и здесь.
Привет, ThirteenAG!
Я, увы, сделал решение по мере своих сил и возможностей. Решении с Extra Options очень популярно в нашем коммьюнити, и изначально хотел делать через него, но… Extra Options вызывают ряд дополнительных проблем, с которыми мы столкнулись, и из-за которых сейчас стоит вопрос о запрете использования их в целом. Поэтому я преследовал цель оставить исходный код игры в самом минимально измененном варианте :)
Sign up to leave a comment.

Articles