Точность позиционирования объектов в играх: возможные ошибки
Если вы возьмёте тип float/double для хранения координат объектов, то получите плавные движения по экрану за счёт сохранения дробной части — игровые сущности смогут передвигаться по субпикселям, но дробные координаты сыграют с вами злую шутку когда лимит точности float будет исчерпан — вы получите резкие движения по игровому полю, фон так же будет дёргаться если его смещение тоже сделано через дробные координаты. Ниже пример из старого Майнкрафта при координатах более 12 Млн. (у кого Ютюб не робит, короче там всё дёргается будто пинг 400 мс. хотя это одиночный мир)
Какие я знаю способы это пофиксить:
Использовать целочисленные «глобальные» координаты + дополнительные «локальные» для хранения дробной части, думаю предел int64 вы не скоро преодолеете. Пример таких координат в коде:
struct Cord {
int64_t ix;
int64_t iy;
float fx;
float fy;
};Использовать координаты с фиксированной точкой после запятой — так в любом месте игрового мира вы будете передвигаться с одинаковой точностью без скачков, но если у вас много вычислений связанных с такими координатами, вы получите только тормоза;
Не менять float координаты, а двигать весь мир вокруг игрока — так поступили в Kerbal Space Program. Игрок будет оставаться рядом с центром координат и не выходить за пределы точности, а фон/противники/объекты будут пододвигаться к нему поближе. Так я сделал в своей игре, просто ставлю игрока в {0, 0} и когда он двигается, добавляюсь остальным объектам его смещение.
Можно так же использовать int64_t для координат, а для имитации плавности в субпикселях можно делить координаты на 100.
Напишите свой способ или как там сделали в других играх, мне интересно.
Приведу пример движения квадратика по кругу с разным удалением от нулевых координат. Позиция квадрата записывается через struct Vec2 {float x, y; };
. На второй гифке видно что движение происходит не по кругу:
Летает рядом с центром координат {0, 0}
Смещение квадрата от центра {-1'124'524, 11'142'415}
Для float координат нет никаких гарантий совместимости
Вы можете подумать что работа с float pointing регистрами стандартизирована, но нет никаких гарантий что какие-либо правила обработки float чисел будут нарушены, вот пример на GCC, который ведёт себя по разному в зависимости от опций оптимизации:
struct Point {
float x {};
float y {};
};
constexpr float pow2(float x) { return x*x; }
Point simple_physics() {
Point pos {0.12445f, 1244.55311f};
cfor (_, 240*60*30) {
pos.x += 0.00124f;
pos.y += -0.00007f;
auto len = std::sqrt(pow2(pos.x) + pow2(pos.y));
if (len != 0) {
pos.x /= len;
pos.y /= len;
}
}
return pos;
}
Результат simple_physics для -O0:
.x = 0x3f7f97d6u
.y = 0xbd66d891u
Результат simple_physics для -O3:
.x = 0x3f7f97d5u
.y = 0xbd66d983u
То есть тут дело в функциях sqrt/sin/cos и прочем из стандартной библиотеки, если вы хотите чтобы вычисления выполнялись по всем стандартам, то вообще не используйте оптимизации и собирайте с -O0, но этого вы терпеть не станете. Если ещё вариант использовать программную эмуляцию работы с дробными числами, но она будет ещё медленнее. По итогу для совместимости надо собирать игру с одинаковыми флагами отпимизации типа -m64 -Ofast -march=x86-64
и не делать никаких других релизов, ведь различия в матане будут прослеживаться даже между x32 и x64 билдами — по этой причине в Factorio отказались от поддержки x32, потому что было сложно сохранить совместимость для сетевой игры, это было чревато рассинхронами из-за несовпадающий чисел.
Выводы
При использовании float координат для позиционирования объектов, вы встретитесь с багами на дальних расстояниях от центра координат игрового мира.
Работа с числами с плавающей точной оптимизирована на процессоре, поэтому софтверные аналоги с сохранением точности могут не подойти для вашей игры из-за тормозов.
Если у вас есть билды игры под разные платформы и с разными уровнями оптимизации, вы столкнётесь с рассинхронами в сетевой игре или при воспроизведении реплеев. Даже может произойти ситуация, когда игрок на одной платформе вынесет всё hp боссу быстрее, чем игрок на другой платформе.
Можете пойти путём дедов и юзать integer для координат и городить поверх субпиксельную плавность добавлением дополнительной переменной субпиксельных координат или делить координаты на 10 или 100 (или <<= 4), тогда у вас будет всё быстро и стабильно, но вы уже ничего не поймёте в своём коде...
Можно поделить мир на чанки и привязывать объекты к локальным координатам в чанках, тогда при любом размере мира вы не встретите дёрганья по экрану.