floating point in networking (noob)

Started by
7 comments, last by hplus0603 11 years, 11 months ago
Hi
Ive understood floating points (float, double) is non-reliable. Im doing a lock-step realtime strategy game.

But dont i need it? For positioning on the map for example i have armies moving around in realtime. They are not on a specific tile and even if they were i would use some floating point to know the delay until moving to next tile (since i need variable moving speed for different armies).

Lets say i use a lock-step lenght of 100ms (each player can send one commandpackage per step) and each world updates itself due to this commands.
If I use ANY float/double for describing the world it will become unsynched after a while? This seems hard to overcome.

Thanks for your help
Erik
Advertisement
I've understood floating points (float, double) is non-reliable ... If I use ANY float/double for describing the world it will become unsynched after a while?
No this is not true. Floating point math is strictly defined by the IEEE-754 standard, and if you run the same code 100 times, you will get the same result 100 times.
You've just got to make sure that every client is running the exact same code...

The problem with reliability is that different compilers (or the same compiler when given slightly different code or settings) may produce different floating-point instructions -- each individual build will be reliable, but two different builds may produce slightly different results.
So a user playing version #1 of your game may generate different results than a user playing version #2 -- this is usually only a problem if you're trying to play back old replay files, or if you support cross-platform play (e.g. Windows is version #1, MacOS is version #2).
However, as long as all of your compilers are set to "strict" floating point mode, then they will all produce the same results.

If you want to learn more about floating point, check out this series of articles: http://www.altdevblo...floating-point/
If you use 1.0f = 1 second, then there's no problems. Float provides 0.1 accuracy till 100,000 or even 1,000,000. Do you expect someone to play over 27 hours non-stop? (100,000 seconds = 27 hours).

Alternatively, you could store timestamp is 64bit integer. That'll provide good accuracy and you can make it "circular" (if it goes over 64bit limit it just goes back to 0 like nothing happened):

LSVOID LSE_CALL CTime::Update( LSBOOL _bUpdateVirtuals ) {
LSUINT64 ui64TimeNow = GetRealTime();
LSUINT64 ui64Dif;
// Handle wrapping gracefully.
if ( ui64TimeNow >= m_ui64LastRealTime ) {
ui64Dif = ui64TimeNow - m_ui64LastRealTime;
}
else {
ui64Dif = (0xFFFFFFFFFFFFFFFFULL - m_ui64LastRealTime) + ui64TimeNow + 1ULL;
}
m_ui64LastRealTime = ui64TimeNow;
UpdateBy( ui64Dif, _bUpdateVirtuals );
}

Source: http://lspiroengine.com/?p=378

If you use 1.0f = 1 second, then there's no problems. Float provides 0.1 accuracy till 100,000 or even 1,000,000.
Ow... careful. What is the representation of 0.1 in powers of two? 1/10 = 1/16 + 1/32 + 1/256 + 1/512 + ...?

You really cannot represent any fractional number in any range, even if it has "only" one digit, and a float has so many. 1.0 works, as do 0.5 and 0.25 and 0.125 and so on, or any sum of these or any power-of-two multiple. Anything else, like 1.1 or 44.7 is kind of nasty to represent.

(p.s.: Also, I know people who play online games 35+ hours before fainting. Yes, this is scary, but such people exist.)

So a user playing version #1 of your game may generate different results than a user playing version #2 -- this is usually only a problem if you're trying to play back old replay files, or if you support cross-platform play (e.g. Windows is version #1, MacOS is version #2).
However, as long as all of your compilers are set to "strict" floating point mode, then they will all produce the same results.

If you want to learn more about floating point, check out this series of articles: http://www.altdevblo...floating-point/


This is sadly not something to rely on in the real world. While IEEE-754 is rather tightly defined regarding what you have to be able to do, it is not terribly strict about giving you a bit more bits on intermediates at times. Intel in particular had a tendency to do things at 80 bits internally, no matter what. While you could indeed force the precision (and rounding modes, don't forget those) with the floating point control register, you never know where that register has been. DirectX liked to stomp on it in every call back in the DX5 days, might still do that.

Aside from the CPU differences (and the loads of fun getting an AMD and an Intel chip to agree to the bit on results) there's the minor detail that "strict" mode is not exactly a well defined industry standard. While most compilers have some set of options that can turn on such a mode, that still probably won't make them generate 100% identical code, and while you can sort of work around that by using the same compiler on all platforms, good luck finding a visual studio that will spit out code you can link on mac os x.

If you want to be certain, your options are:

Make one of the clients authoritative, and be able to resync the game state if it's found to have drifted
Use fixed point for (absolutely) everything simulation related

A popular RTS game in years past had unsolvable problems with cross platform networking due to FP issues. Most of them were papered over by rounding off every float that went over the wire to ~12 fractional bits. This was not enough, as the AI and/or pathfinding code had some float comparisons that had different numbers of calls to the (synchronized) RNG in the two paths. Eventually one system would go one way, the other would go the opposite way, and the game would explode.

Don't be like that game.
If you run the same code on all the machines, using the same architecture, and control for variables (like FP rounding modes, 64-bit vs 80-bit double registers, etc) then lock-step simulation with floating point is possible and efficient. Note that some graphics APIs or other libraries may change the floating point control word of the CPU, so you have to test-and-set it at the beginning of your simulation step. Also, trying to do simulation between, say, ARM and x86, will not work deterministically, because the architectures do FP differently, even though both are IEEE compliant.

For timing, you want to use an integer that counts even simulation ticks -- do not use floating point for timing, or your physical behavior will subtly change at the end of an 8-hour game compared to at the beginning. (It can still be deterministic across machines, but I'd prefer to also be deterministic across time.)
enum Bool { True, False, FileNotFound };
It will not be crossplatform and only use same builds. And use directx as well. No game will be longer then 3 hours for sure. So floating points should be ok then?

Also is there difference in precision for storing something like 13.421 in a float or double? I know double has more bits but does this matter when there is not that many decimals of "real value" anyway?

To know when its time to for next step in my lockstep would i use internal clock? And store that in something like a long (counting milliseconds i guess). My planned timestep will be between 50-100 ms.

Thanks for your help!
Erik
Number of decimals in base 10 doesn't really relate to number of decimals in base 2, as pointed out by samoth in his example.

And it also depends on how big your integer part of the value is.

I find it more useful to visualize it as markers on a ruler, where you get fewer and fewer markers (less bits) "in between" the whole numbers, the higher your integer part gets.

Also is there difference in precision for storing something like 13.421 in a float or double? I know double has more bits but does this matter when there is not that many decimals of "real value" anyway?


Here's a specific example to add to what others have said:
1.1 in decimal is approximated as
1.10000002384185791015625 as a 32 bit floating point and
1.1000000000000000888178419700125232338905 as a 64 bit floating point
One useful technique is to use fixed-point values. A 32-bit integer can be seen as a 24-bit integers with 8 bits of binary fractions, or 16-bit integers with 16 bits of binary fractions. (Or 20-bit integers with 12 bits, or...)
If you're using C++, you can build a wrapper class Fixed which does the right scaling for multiply/divide, and it'll be almost as easy as using floats/doubles -- except it will be much more robust across systems in lock-step mode. In fact, for integer, with a proper Fixed template class, you can likely get perfect sync even across CPU architectures.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement