Support Client-Side Prediction
- Determinism
- Rollback
- Contact Begin/End events
A comment here from our use-case that we've been dealing with in various ways over the past few years... For reference, a project with Box2D Client-Side Prediction implemented here (Also added SDF collision fixtures to this as well): https://rocketbotroyale.winterpixel.io
But for client-side prediction to work, we need to arbitrarily rollback the world to some given state and re-simulate with step() X number of times applying some set of saved inputs per tick. Let's call this process, Rollback & Resim. This rolls the simulation back to some point in time, then fast-forwards back to the real time stream.
This actually works very well in Box2D with a few caveats...
-
The first is that since the engine uses linked lists, the order of the internal lists is important for determinism. So if you're rollback involves say removing and adding bodies to the simulation ( think rockets, missiles, etc ), then after the reset you need to ensure the internal b2 lists (b2body list for example), is in the same order as the server. We can work around this with sequence number on body creation and insertion into world. And a simple re-sort on the reset.
-
Second is that resetting the world via this mechanism usually means rebuilding the contact list. Before we reset anything, we ignore all contact listeners (Begin,Pre,Post,End), and disable any processing there. Then we reset/recreate all bodies and fixtures to known state, sort internal lists, then manually call
FindNewContacts()to build the internal contact list. The idea here, is that afterFindNewContacts()is called on the client's reset, the client's world state should be the same as the server after it's regular step function(). We've found this generally to be correct with the exception of Begin/End contacts state being contact state that crossesstepboundaries. It would be ideal if you could remove this kind of inter state contact dependency as it makes the reset part much easier. Right now Box2D never will emit aBeginContactandEndContactin a singleStepeven after the solver finishes there is no AABB overlap, i.e. the contact doesn't exist. TheEndContactcallback is only emitted on the nextStepcall, which means there is leftover contact state between two world iterations. I think eliminating any type of inter step state would make it easier to reset simulation to arbitrary points in time, in which client-side prediction with rollback & resim becomes a lot easier to do.
Just another comment as this scenario may be helpful... We run our cloud gameservers single threaded because we generally want our server load to be as non-volatile as possible. i.e. I'm not sure we would WANT to solve our physics simulations multithreaded there. For our use-case we schedule each gameserver a core (well less than a core in all honesty), but we definitely limit the cpu on each gameserver to 1vCPU.
So multi-threaded-solver... Very welcome on a client device, but i'm not sure I would want to run the solver on multiple threads on a gameserver. We'd want to pin the cpu through the simulation on a single core so we can balance load across a cloud VM with reasonable cpu scheduling assumptions for each gameserver process.
Would be great if when thinking about determinism and threading, if some different configurations were available.
Will this be targeted for 3.1? Or is it more of an investigation task?
I'd like to get this in for v3.1. However Rollback might be v3.2. It is a big feature.
This has gotten me thinking again... Just a note for any potential rollback feature in b2c, but we recently shipped another game with client reset/rollback, however on this game we had a lot of objects that were stacked and there just wasn't a decent simulation that didn't kill our cpu without warm starting enabled.
So we have it enabled and eventually our client simulations just kinda even out, but this has gotten me thinking about warm starting in general and how it would apply to rollback/reset simulations as well as any other internal 'state' memory that the engine maintains across Step()s. I don't have anything to really contribute here other than recognizing that once you get into the internals of warm starting, determinism and rollback becomes seem very complicated when it involved any internal state shared across world ticks.