0
\$\begingroup\$

From what I've read, there's several components which go into rollback netcode and requirements.

1: Your game logic update (input/state update) has to be able to run independently of your rendering (draw stage/textures/etc) I think 7 times as fast?

2: The connections need to run over UDP p2p.

So my first question is, if my game draw is updating at once every 60 frames, does that mean my game logic, input, network read, has to update once every 8-9 fps?

And I imagine the loop looks something like this?

if(framecount%8==0){

pollnetwork();
updategamestate();
input = checkinput();
 if(input){
  sendinputacrossnetwork(input);
 }
}
if(framecount%60==0){
render();
}

I imagine the order in which those are called is irrelevant.

So I have that in place, but where do I go from there?

Do host and peer first have to synchronise gameframes? Then every time a packet is sent between the two, they have to check what frame they're currently on, which frame they received then rewind and replay?

For example, if host is on frame 18 then won't the client frame when received be on frame 9? Because it's always updating, say 9 frames. And if the server receives client's frame 9, we go back 9 frames and redraw everything from 9 frames ago to the current state depending on what the user provided as inputs?

There's a lot I don't understand about rollback netcode and I've read a lot too but there's small implementation details that I can't figure out. I might even be missing the bigger picture.

Edit: To give more info on this, I'm doing this in C++. I'm not using a game engine, rather writing it myself in SDL with GLFW and my own network library. And in my case the state should never change during the time of the render, it's all running on one thread. And since my network function returns instantly whether or not a packet is available, I dont' have to rely on callbacks.

I have my logic update decoupled from my draw rendering. So I can separate the two.

But I think I understand it now, if I have it correct:

I have an input structure and a frame stack. Assume the frame is currently at 30

30:[peer input: null][my input: x]
29:[peer input: null][my input: y]
28:[peer input: null][my input: null]
27:[peer input: null][my input: null]
26:[peer input: null][my input: null]

Now I receive a packet with frame# 27 and an input x, because I'm already on frame 30, I now have to rewind my frames back to frame 27, inserting an input x and assume that they're going to keep pressing x until I receve a frame that says otherwise; So I rewind the framestack and insert the x where it belongs while leaving my input untouched

30:[peer input: x][my input: x]
29:[peer input: x][my input: y]
28:[peer input: x][my input: null]
27:[peer input: x][my input: null]
26:[peer input: null][my input: null]

Now I run the logic loop 4 frames simulating the state as if they had pressed x each of those frames? Then I finally draw?

\$\endgroup\$
3
  • \$\begingroup\$ Rollback as a concept can happen at any rate and over any connection type... The key point is that the system you're replicating will proceed on available information, but when it gets data/an event from the client, if it materially changes the situation, everything since the event happened is discarded and the simulation re-run to the present time. \$\endgroup\$
    – Basic
    May 8, 2022 at 19:54
  • \$\begingroup\$ @Basic So if I have a frame stack where my game is simulating a frame pushing it onto the stack, I get a frame that doesn't allign with a frame on the stack, I overwrite nth frame, set my current frame back to n and rerun the simulation to the most recent frame? Then my draw is called on the most recent frame? \$\endgroup\$
    – Kayla
    May 8, 2022 at 20:32
  • \$\begingroup\$ To separate your simulation tick from your display update, you'll likely want to Fix Your Timestep rather than relying on modulo checks like you have there. This will let the game run at a variety of display framerates rather than a single fixed ratio of simulation:display updates. \$\endgroup\$
    – DMGregory
    May 8, 2022 at 22:02

1 Answer 1

1
\$\begingroup\$

First up, the state of your game should be decoupled from your rendering.

The rendering loop should query the game state and display it on screen.

A separate loop (sometimes called a physics tick) is where you handle changes to the scene.

Many physics engines require consistent time steps between iterations, and other background tasks (like AI) shouldn't be impacted by how good the user's graphics card is.

Depending on what you're doing, this may require a little coordination (thread signalling) to ensure the game state isn't changing at the same time as the renderer is trying to display it.

You haven't specified what technology/engine/language you're using, nor the specific problem you're trying to solve, so let's pick an example and say there's a log rolling down a hill.

By default, the log will roll until it hits a door and breaks it.

It's possible for a player to shoot at the log and divert it.

You server has a physics tick every n milliseconds.

In that tick you calculate the movement of the log due to known forces.

If you receive a packet saying "the player fired at this spot at time X", you need to:

  • "Rewind" the simulation to the first tick that would've been impacted by the event
  • Add the physics effect from the shot to the simulation
  • Run the simulation forward to the present time [possibly re-processing every intervening tick], being sure to handle all other player input you've received so far.
  • Do it all quickly enough that it's not perceptible.

How exactly you do this is implementation- and use-case specific.

Some systems save state data for the last n ticks, so they can just discard everything since then and start from a known good state (can be memory intensive and puts a hard limit on how late packets can arrive).

Others have a physics simulation that can be run in reverse deterministically, so they can run time backwards and forwards without the simulation becoming unstable.

Yet others use clever algorithms to invalidate/recalculate only those physics objects potentially impacted by the event, cascading the calculations as needed.

However you do it, when you've worked out the world state, you update it in memory and the render picks it up. The log will appear to teleport from where it was to where it should be.

\$\endgroup\$
5
  • \$\begingroup\$ Thank you for that information. I have added extra to my post \$\endgroup\$
    – Kayla
    May 9, 2022 at 3:15
  • \$\begingroup\$ Your edit is pretty much on the nose (except you really need to stop thinking of these as frames). One thing... You say "Assume X remains pressed until told otherwise".... That might be a good assumption or not, depending on what users normally do. If X is usually pressed for a single tick, you might want to "guess" it'll be released immediately. If that's what actually happens, great, no work required. If not, it's another rollback. Implementing this yourself is non-trivial and there are a LOT of nuances and optimisations. \$\endgroup\$
    – Basic
    May 9, 2022 at 11:38
  • \$\begingroup\$ Doing this well is one reason Valve's game engines became so popular. To give you an idea, have a look here: arstechnica.com/gaming/2019/10/… and developer.valvesoftware.com/wiki/Source_Multiplayer_Networking \$\endgroup\$
    – Basic
    May 9, 2022 at 11:39
  • \$\begingroup\$ ah okay, thank you I'll have a look at those! \$\endgroup\$
    – Kayla
    May 9, 2022 at 17:40
  • \$\begingroup\$ Just happened to stumble across this GDC talk which is somewhat relevant and may be of interest: archive.org/details/GDC2015Fiedler (GDC 2015 Glenn Fiedler - Physics for Game Programmers : Networking for Physics Programmers) \$\endgroup\$
    – Basic
    May 12, 2022 at 17:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .