How do you manage state when client ticks aren't as fast as server ticks?

Started by
8 comments, last by hplus0603 3 years, 10 months ago

Given a setup where your client is predicting (tick 100) the actions that they think they will be doing once the server (currently tick 75) receives their input. When the client receives the world state from the server once it reaches and sends the results for tick 100, the client goes back in time to make sure it didn't mispredict. If it does, it has to do a full rollback to that tick (100) and repredict all the inputs up to the current client prediction tick (125 in this contrived example).

Let's assume the client has perfectly predicted everything, it simply wouldn't rollback. It would continue predicting forward. But, the client may not always be able to predict each individual tick like the server is guaranteed to run. Say your server is 60hz and your client up until this point has run at 60FPS. Suddenly, the client is running at 30FPS due to hardware slowdown - either rendering or maybe it's a PC or phone with thermal throttling. Now the client is running at half the speed, so therefore it can only do half the ticks.

I've seen setups where the client simply then does 2 (or more) ticks in 1 frame, in hopes that the slowdown was temporary and the device will regain 60FPS performance again. In my experience, however, this is extremely fragile because more often then not, the device may never recover the performance. And once it does, it enters the dreaded “death spiral” where every frame it's accumulating an increasing debt of ticks.

The example here: frame 1 has to do 2 ticks but it's so slow to do both that now 3 ticks have passed by the time frame 2 hits. So now frame 2 has to do 3 ticks, then frame 3 has 4 ticks, etc

So I was thinking about and even read somewhere that, well, maybe the client doesn't have to predict every tick. Yes, it's ideal, but you should still be able to get the same results (or super close) to what the server will say is reality, hopefully therefore not having to do a rollback.

The example here: If the device slows down, and 2 ticks pass in 1 frame, then you just predict 1 tick with a delta time of 2 ticks. You may be left over with a “fractional tick” that you can just accumulate until it reaches a full tick. Glenn Fielder has a blog that mentions this approach.

  1. Is this a sound approach?
  2. Does anyone here have experience with this and know of "gotchas" to look out for?
  3. This last question is a bit technical. I can best explain it as an example.
    A player is moving forward and predicts it accurately even with a slowdown. However, something happens: the world has changed or an event occurred (floor changed or player is hit/damaged) that makes the player suffer a movement “slow”. You can't predict that, so you do a rollback. However, your system is saying “damage was taken at tick 100. So on tick 101 you must move slowly.”
    Given that we are “lumping” ticks together when a device gets slow, this logic is no longer valid. A client may never hit tick 101. If the client rolled back to tick 100, and then in the next frame they've skipped over to 102 or 103 tick and beyond, they can't apply that slow properly can then? Anyone have any smart solutions to this? I'm thinking instead of doing “if (tick == damagetick + 1) { slow(); } “
    do ”if (tick ≥ damagetick + 1 && !triggered) { slow(); triggered = true; }"
    This however I feel is perhaps fragile and isn't accurate. It'll just lead to mispredictions after you've already mispredicted the damage being taken or whatever even occurs. Please let me know if anyone has tricks or resources that deal with something like this!
Advertisement

Usually, most of the time is spent for rendering if everything runs fast enough.

If your game requires rolling back 25 ticks, then your game simulation should run fast enough to do at least 25 ticks in a frame while having enough spare time for rendering. I read some modern fighting games actually do resimulate 7 or 10 ticks each frame.

If you start doing calculations different on the client than on the server, then it will differ even more from the reality on server, it will make your code more complicated and you will need to resimulate everything on each server update the client reeives.

If your device is not capable enough, the easier way is to not predict and just show remote actors in the past. Or just disconnect clients lagging behind too much.

wintertime said:

Usually, most of the time is spent for rendering if everything runs fast enough.

If your game requires rolling back 25 ticks, then your game simulation should run fast enough to do at least 25 ticks in a frame while having enough spare time for rendering. I read some modern fighting games actually do resimulate 7 or 10 ticks each frame.

If you start doing calculations different on the client than on the server, then it will differ even more from the reality on server, it will make your code more complicated and you will need to resimulate everything on each server update the client reeives.

If your device is not capable enough, the easier way is to not predict and just show remote actors in the past. Or just disconnect clients lagging behind too much.

Definitely intend to have a disconnect or “catchup” system for clients who lag behind too much. We'll pause their simulation & prediction to just wait until it acks enough server states to be all caught up, then resume predicting. Any junk like the tick debt I mentioned would just be discarded including any further input until their sim is ready to go again.

But for low end devices, some of my biggest chunks of frame time is simply doing physics calculations. Like 3D movement with rigid bodies is a huge sink. I'm using Unity and their stuff is designed to run once a frame. Doing 2 or more movement calls in 1 frame just murders even high end phones. I could maybe try to replace with movement with something handrolled, but I feel like thermal throttling is just going to get in the way of players who want to play for extended periods of time on mid and lower range spec phones. It's weird to have a great experience when you do your first 10 minutes of gameplay only to have it degrade when the device gets hot, which unlike a singleplayer game, will snowball into being unplayable due to the tick debt. Vicious cycle of death haha

NetworkDev19 said:

  1. Is this a sound approach?

Not really, in my understanding at least. As you get at with your third question, there are situations where the in-between ticks are vital. Collision is the classic example, where the client would let a player walk through a wall that the server stops them at. You'll then need different checks on the client, at which point you'll be trying to hit the same deterministic sim with two ever-varying sets of code.

The simple solution would be to drop your sim rate on both sides, such that you don't run into performance issues. If you're rendering at 60fps, simming at 30 iterations/second is pretty normal.

This part is just my opinion from thinking about the problem for a few minutes: if you really need to deal with variable performance, you need enough wiggle room to go in either direction. For any bit of slowdown, you need to eventually be able to speed back up. If you're planning on eventually hitting thermal throttling, I would assume that you need to target the throttled specs as your “minimum” from the get-go. e.g. at the throttled specs, we're able to run at least 2 simulation iterations within our time budget.

NetworkDev19 said:

The example here: If the device slows down, and 2 ticks pass in 1 frame, then you just predict 1 tick with a delta time of 2 ticks.

Careful with this part, if you do end up going forward with this. Float consistency between systems is already dubious, but straight-up using double the delta would likely throw determinism of the physics sim out the window. Maybe there's a way to account for that, I'm not a physics guy.

Archduke said:

NetworkDev19 said:

  1. Is this a sound approach?

Not really, in my understanding at least. As you get at with your third question, there are situations where the in-between ticks are vital. Collision is the classic example, where the client would let a player walk through a wall that the server stops them at. You'll then need different checks on the client, at which point you'll be trying to hit the same deterministic sim with two ever-varying sets of code.

The simple solution would be to drop your sim rate on both sides, such that you don't run into performance issues. If you're rendering at 60fps, simming at 30 iterations/second is pretty normal.

This part is just my opinion from thinking about the problem for a few minutes: if you really need to deal with variable performance, you need enough wiggle room to go in either direction. For any bit of slowdown, you need to eventually be able to speed back up. If you're planning on eventually hitting thermal throttling, I would assume that you need to target the throttled specs as your “minimum” from the get-go. e.g. at the throttled specs, we're able to run at least 2 simulation iterations within our time budget.

NetworkDev19 said:

The example here: If the device slows down, and 2 ticks pass in 1 frame, then you just predict 1 tick with a delta time of 2 ticks.

Careful with this part, if you do end up going forward with this. Float consistency between systems is already dubious, but straight-up using double the delta would likely throw determinism of the physics sim out the window. Maybe there's a way to account for that, I'm not a physics guy.

Thanks for your input, Yeah, originally when I implemented it, it seemed to work great. I honestly haven't seen many issues with it but that doesn't mean they don't exist or are having knock on effects for other bugs I'm just not aware of. So I'm weary now that I'm revisiting the topic. The collision checks is one of those concerns that's probably an even better example.

Float consistency was on the mind too, but I already kind of pushed that under the rug a bit. In my testing, floating points have been extremely consistent or at worst very close to a degree they could just be ignored. I already do quantizing on my floats so I'm sacrificing precision everywhere anyway. And when I inevitably accumulate floating point errors, I just rely on the rollback. When considering rollback, I check things like “if position is 5% off ignore it and carry on, but if it's 10% off do a bit of correction, and if its higher then nuke the whole state and rollback" Just like the lumping of ticks together, it's worked so far but it doesn't mean that it'll hold up under scrutiny later with players and full load or questionable devices or network.

Giving me a lot to think about. I'm really targeting 60hz, or even higher and possibly trying for the ridiculous 128hz as like a wish list type thing. Of course it's easier said than done. But, physics be damned - killing my performance :P

NetworkDev19 said:

Giving me a lot to think about. I'm really targeting 60hz, or even higher and possibly trying for the ridiculous 128hz as like a wish list type thing. Of course it's easier said than done. But, physics be damned - killing my performance :P

Best of luck! You probably already get this, but I just want to stress again, we moved away from linking render and sim rates a long time ago (it's why we deal in delta time in the first place). I'm not familiar with Unity so I don't know how it manifests, but it would be really unfortunate engine design if you couldn't run the physics sim slower than the render rate. Doing so doesn't make anything feel worse as long as the sim is at a reasonable speed, since the renderer lerps between the previous and next states. Additionally, network tickrates can be even slower than both of those (you just batch messages).

You will want to fix your timestep if at all possible, and run multiple simulation ticks between render ticks if your renderer falls behind.

Some game engines (cough unreal engine cough) have “one simulation tick, one frame” pretty hard coded in them, and at that point, you can't perfectly predict what the server will do. You'll just have to accept the fact that clients that are too slow, will end up with corrections.

enum Bool { True, False, FileNotFound };

Archduke said:

NetworkDev19 said:

Giving me a lot to think about. I'm really targeting 60hz, or even higher and possibly trying for the ridiculous 128hz as like a wish list type thing. Of course it's easier said than done. But, physics be damned - killing my performance :P

Best of luck! You probably already get this, but I just want to stress again, we moved away from linking render and sim rates a long time ago (it's why we deal in delta time in the first place). I'm not familiar with Unity so I don't know how it manifests, but it would be really unfortunate engine design if you couldn't run the physics sim slower than the render rate. Doing so doesn't make anything feel worse as long as the sim is at a reasonable speed, since the renderer lerps between the previous and next states. Additionally, network tickrates can be even slower than both of those (you just batch messages).

Yeah, Unity has some massive performance hits on its character controllers and physics stuff when you try to move them. In order to do prediction properly, I need to move at least the player's physics in each tick in order to properly predict the movement. So each time I hit that “apply movement, let us check the resultant position for the next tick”, it hurts the frame time. Do it twice or three times and you're done for. There are external kinematic solutions available but I just haven't gotten around to investigating them. They may be my best solution instead of messing around with weird tick skipping logic.

Some game engines (cough unreal engine cough) have “one simulation tick, one frame” pretty hard coded in them

Wow, I did not know this. I presume that's their built in networking stack and if you wanted to do it differently, you'd have to make your own?

Do it twice or three times and you're done for.

That sounds wrong to me. Sweeping a few capsules through some collision geometry shouldn't take any perceptible time. Have you profiled this, and seen what's going on? My guess is that it's doing a whole lot more work than necessary, and perhaps there's a way to make it not-do-that.

The unreal variable time step goes more to the physics system than the render system – they don't want render time to be different from physics state time, so because they support variable render (or a render rate that's not strictly an even multiple of physics,) they have to make physics not use a fixed timestep. The alternative is to either render the latest-simulated with some time discrepancy, or render a an extra- or interpolated physics state.

Because their physics works like that, they in turn make the networking based on sending object trajectories and extrapolation, rather than sending command inputs and re-calculating. They also send inputs for some things, but there's a significant amount of bandwidth used just to push object transforms around.

Then again, “significant” varies over time. If there's 20 bytes of transform state per player per update, 20 updates per second, and 100 players, that's 40 kB/second, which was HUGE back in the day, but now is't that different from a single high-quality music stream. Send far-away players less often, and you're pretty much good enough, before even bit-packing or delta-encoding or whatnot.

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement