Если вы возьмёте тип 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; };
. На второй гифке видно что движение происходит не по кругу:
Для 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), тогда у вас будет всё быстро и стабильно, но вы уже ничего не поймёте в своём коде...
Можно поделить мир на чанки и привязывать объекты к локальным координатам в чанках, тогда при любом размере мира вы не встретите дёрганья по экрану.
Комментарии (17)
ALapinskas
27.01.2025 05:28Вычисления с float еще и медленные по сравнению с int.
Solarian_Guide
27.01.2025 05:28На современных системах благодаря различным оптимизациям, инструкциям и векторизации разницы уже нет, а зачастую float даже быстрее при умножении/делении, чем int.
Jijiki
27.01.2025 05:28это надо запускать Юнити и проверять ), так на словах скептично звучит ), я к тому что чтобы это всё продемонстрировать всё надо показывать на примерах), почему к примеру Юнити так будет понятно какая математика, что где и прочее ), потом болячки движков еще окажется разные, у Майнкрафта может быть своя математика, движок, в самой математике еще есть нюансы поидее, плюс тут база математика, она вляет на физику а там нюансы тоже, потом не понятно какой должен быть мир чтобы с этим столкнуться, на поверхности хорошо, но будет ли мир 1 милион размера это даже представить не возможно тоже нюанс, потом почему - + , может у кого будет ++ или --, по ощущениям в том же Ухты не 1 милион в калимдоре, и на берегах некоторых прогрузка+люа что-нибудь, вообщем нюансов больше, потом если сетевая игра, и может на других устройствах точность побольше чем на стационаре
Jijiki
27.01.2025 05:28можно уйти от ухты и ТЕСО, и посмотреть на Скайрим, Морровинд, и где тоже есть глитеффект - дагерфол вроде или арена, в ультиме например, которая онлайн тоже бесполезно смотреть потомучто локальную точность может пофиксить сервер поидее, в фалаут76 например не 1 миллион мир, ну Старфилд получается может подпадать, но поидее они с этим сталкивались на одной из первых игр и теперь локальные игры либо прогрузка+мир, либо как мороввинд либо как фолыч, тут всё очевидно по решениях балансируем между прогрузкой и точностью ну поидее
Jijiki
27.01.2025 05:28а как же фракталы работают тогда, там мондельброт какой-нибудь, визуально бесконечность.
Jijiki
27.01.2025 05:28простите может я чтото не понимаю проверил у себя, (vec3){1000000.0f, 0.0f, 1000000.0f} и камера и кусочек террейна (800,50,800 - масштаб 40к вершин), камера крутиться и летает, террейн не трясёт в -+ трясёт террейн и камера локается толи из-за эпсилоны(проверка) толи не знаю почему, (так же проверил масштаб (800х3)х(800х3) - это очень много, там можно на таком масштабе чтото сделать, плюс хитрость в том, что минимизировать потери же можно если центр 0 0 0 будет в центре тоесть раскрыть 9 тайлов и туда и туда, или только в +, тоесть можно заползти террейнами на минусы и планомерно уходить в +, там на любую игру хватит
AtariSMN82 Автор
27.01.2025 05:28А не работают, если брать стандартные алгоритмы вычисления и даже использовать для них long double, то при глубоком увеличении картинка становится квадратной кашей. Посмотре на ютюбе несколько видосов с увеличением внутрь фрактала, перемотайте все 10 часов видео до конца и вы увидите как картинка превращается в однообразную заливку. Вообще это тоже фиксится специальным алгоритмом, который показывает фракталы детально на любом увеличении
lear
27.01.2025 05:28А в чём Профит 3 способа?
Я знаю что такой подход использовали когда нужно перемещать персонажа другими объектами (к примеру лифт)
Т.е. явно 3 способ включает не только инверсию манипуляции координат, но и ещё что-то, ведь если у вас есть вероятность выхода за пределы точности персонажа, то есть и вероятность выхода за пределы точности окружающего мира?
Или у нас получается так, что окружающий мир будет находится так далеко при достижении пределов точности, что мы его не заметим? Т.е. перекладываем этот момент на движок?
AtariSMN82 Автор
27.01.2025 05:28Фишка третьего способа в том, что мы не меняем float для позиций и не трогаем код вычислений. Пока игрок смотрит на себя - всё хорошо, объекты за экраном могут позиционироваться грубо и дёргаться, но этого никто не увидит. Подходит только для игры где игроки в пределах общей камеры
orefkov
27.01.2025 05:28Сталкивался с таким в шейдерах на мобильниках, там float вообще может быть 16 бит. Делал процедурный фон, анимировался параметром через время в float секундах. На компе норм, на мобилках через несколько минут начинало всё дёргаться. Пришлось этот параметр на мобилках зацикливать, крутил от 0 до 30, потом обратно от 30 до 0.
AtariSMN82 Автор
27.01.2025 05:28В ps1 вообще позиции вершин на видяхе были в int16, поэтому-то там всё так плавает и все стены кривые
AKudinov
Но ведь
и есть, по сути, представление чисел с фиксированной точкой. Только точка зафиксирована неоптимальным, для двоичных чисел, образом.
Rio
Зато очень удобно в работе. Вместо условных метров используем миллиметры, и можно даже не относиться к ним как к фиксед-поинтам. Вычисления становятся молниеносными, и гудбай все проблемы с флоатами. Для простых 2D-игр отличное решение (если на этом остановиться, и не использовать физические движки, которым обычно флоаты всё-же нужны).
Jijiki
2d бывает разный, бывает рейкаст - возможно, бывает через 3д, и бывает просто картинки в окне (глобально координата 1000000,1999999), а локально тайл 100 100, типо того причем локальные тоже могут быть сгруппированы индексом более низким единицами например
опять пришли к нюансам
AtariSMN82 Автор
Ну под числами с фиксированной точкой я имел ввиду всякие библиотеки где это сделано посложнее чем деление инта и можно выбирать длинну дробной части и есть всякие стандарты типа Q16.16