Tips for upgrading a fixed time step game using integers to variable time step and eventually floats?

Started by
10 comments, last by Aybe One 1 year, 1 month ago

Here's a fairly simple problem but for which I'm struggling and looking for some advice.

I have a game that uses a fixed timestep of 30 FPS and integers, I would like to be able to upgrade it to 60 FPS.

In that game, there are constants that are ratios of 30 FPS: 15, 60, etc… many calculations are done against these and it works impeccably… when the frame rate is 30 FPS. (Note: I didn't write the game, just inherited the code base)

Example:

An input value X is an integer of 30, so at the time of update, X / 30 (fps) == 1.

Now if I upgrade the frame rate to say 60 FPS, I've got a problem since X / 60 == 0.

I guess you get the point, some of the values are so low that if I try to divide by more than 30 then it invariably ends up as zero since it's integer arithmetic.

Attempt:

I thought about upgrading the game using floats, I spent one good afternoon in a branch to try upgrade what roughly made sense to upgrade to floats, however that didn't quite work well, the physics went totally crazy! ?

This led me to think about two things:

First it will take significant time to upgrade to float, obviously and potentially for a questionable outcome if not introducing subtle bugs that will be hard to track.

Second thing is, I've been thinking if ain't simply “smarter” to keep using integers as it's already well implemented and there won't be any surprise as the game physics are very stable.

But for the latter, as I explained above, I'm facing a hurdle for which I'm seeking any expert advice.

Thanks for your help ?

Advertisement

Would it be enough to upgrade only the visuals? So each odd frame is an interpolation of two ‘real’ even frames?

Otherwise you could try to increase the resolution of all numbers to a multiple of say 256, to avoid the / 60 = 0 problem.
Basically you could use fixed point numbers, e.g. where the low 8 bits are a fraction of one, and the higher 24 bits represent the same number as before. (In your case you would need only one bit for the fraction, to increase resolution by just two.)
This might help to preserve the behavior of the physics a bit better than porting everything to floating point.
This was pretty common before the Pentium era and good enough for sub pixel accurate software rendering or decent physics simulations.

But it's even more work than the port to float, i guess : /

You mean having different speed for physics and render? If so then I'm afraid things are just too intricated. Not impossible but lot of work upfront.

Multiplying things really appear as the most sensible approach.

I've been trying to understand the next paragraph but I don't quite get it except for that very last paragraph which makes a lot of sense!

Been re-reading over and over your post and just had an idea, there is a set of constants against which they do most of their calculations against: FR7, FR10, FR15, FR30, FR50, FR60, they exactly have the values of their suffix so I'm wondering if simply not multiplying those might do the trick.

Basically, right now I'm trying to see if I can get hold of some silver bullet ?‍♂, if not then I guess I'll have to dive deep.

Gonna give it a try and post my results, thanks for the insightful tips!

Aybe One said:
You mean having different speed for physics and render? If so then I'm afraid things are just too intricated. Not impossible but lot of work upfront.

No, i mean physics and also player input is unaffected, just visuals get interpolated to have 60 fps. This would be very little work, and no risk to introduce bugs.

Aybe One said:
FR7, FR10, FR15, FR30, FR50, FR60, they exactly have the values of their suffix so I'm wondering if simply not multiplying those might do the trick.

Surely worth to try!

Regarding fixed point math, just some explanation:

Let's say we want 16 bits for fraction, then the number one with 32 bit int is 0x00010000.
The number 0.5 is 0x00008000, and 2 is 0x00020000. Baically a matter of shifting bits to the left, so we gain precision for fractions on the new bits on the right.

With such numbers, additions and subtractions work as usual, but multiplication / division becomes something like:

inline int fpMul(int x, int y) 
{
	__int64 z = (__int64) x * (__int64) y;
    return ((int) (z >> 16));
}
inline int fpDiv(int x, int y) 
{
	if (y==0) return (x > 0) ? 0x80000000 : 0x7FFFFFFF;
    __int64 z = (((__int64) x) << 32);
    return (int) ((z / y) >> 16);
}

So we may eventually require more bits than the data type has, to prevent overflow / underfow.
For mobile ARM CPUs i had even written a small assembly routine to do this, back then.

What we get is support of real numbers with integers, but unlike floating point numbers, precision is distributed evenly over the range a number can express.
Thus the behavior is the same as with standard integers, which likely is what you need.
Basically, ‘fixed point’ integer numbers are a generalization of your idea to scale constants, so it may be a good search term to find some tutorials and tricks.

Just had another idea: Maybe it's easy to just make everything twice as large, and update the game as is but at twice the rate.

This appears to be some kind of silver bullet but I fail to grasp the substance of it, I need to write some program to understand?!

I think that both you and I might agree on the fact that there's no point in trying to fight against a system, it's vain, rather, leverage it.
And in this case, the simplest, if not most stupid solutions might just be appropriate.
It took me years to realize that trying to be smart when it comes to programming is the worst possible approach as you always pay the price at some point.

Gonna stick on the most stupid fixes for a first but I absolutely have to grasp that concept of fixed point tricks as I might be missing something potentially very useful!

Aybe One said:
I absolutely have to grasp that concept of fixed point tricks

It's really simple: Scale numbers up by some constant, do precise math, divide by the same constant to convert back results.

Here's a code example, plotting a linear equation:

	float s = 0.3f;
	for (int i=0; i<100; i++)
	{
		float y = i;
		float x = y * s;
		RenderPoint(vec2(x,y), 1,0,0);
		if (i%9==0) RenderLabel(vec2(x,y), 1,0,0, "f:%f", x);

		int s16 = int (s * float(1<<16));
		int y16 = i<<16;
		int x16 = (int64_t(y16) * int64_t(s16))>>16;
		x = float(x16) / float(1<<16);
		RenderPoint(vec2(x,y), 0,1,0);
		if (i%9==0) RenderLabel(vec2(x,y), 0,1,0, "\nfp16:%f", x);

		int s4 = int (s * float(1<<4));
		int y4 = i<<4;
		int x4 = (int64_t(y4) * int64_t(s4))>>4;
		x = float(x4) / float(1<<4);
		RenderPoint(vec2(x,y), 0,.5f,1);
		if (i%9==0) RenderLabel(vec2(x,y), 0,.5f,1, "\n\nfp4:%f", x);
	}

Gives this graph:

We see 16 bits for the fraction is enough to match float visually, but 4 bits is quite off for the example.

I finally understood after making a little test by myself, the value of Y must be shifted and then you get the correct behavior, else it was totally wrong.
Thanks to your graph it's suddenly much more clear! But yeah, as you've mentioned, not necessarily a good move unless it's an absolute necessity.
Still, it's an incredible trick!

Long story short, I just tried to multiply all these FR* constants by two and switched the loop to 60 FPS and it scaled incredibly well! ?
There are a few things here and there that visibly aren't using them but overall it's the most convincing result with the least effort I got.

Now I realize my mistake, I already did quite some of the finely tuned frame-rate independent fixups and will have to undo them.
The most pressing for now is to find proper values in the places lacking these constants and bake them in with one of the constants.

That's the kind of silver bullet I like, incredibly simple but so efficient at the same time ?.

Thank you very much for this conversation, your suggestions litterally led me to the best solution in town! ???

Aybe One said:
Long story short, I just tried to multiply all these FR* constants by two and switched the loop to 60 FPS and it scaled incredibly well!

Haha, seems the devs were quite smart with making those defines. Maybe they already prepared for higher framerate. : )

I confirm that the guys who wrote it were real experts, they exactly knew what they were doing.

Looking at how simple yet efficient their code runs makes me humble, like Quake source code.

This topic is closed to new replies.

Advertisement