3
\$\begingroup\$

I am writing a network game based on Gabriel Gambetta's articles on client-side prediction/server reconciliation. I have client-side prediction down (using Box2D for prediction) as well as server reconciliation (setting the predicted entity's physics state back and stepping up to the current client time, applying forces from input every step).

This, of course, results in desync; prediction error. My trouble is figuring out how to correct the prediction error. I've tried snapping 100% to the position from reconciliation, but this looks very jumpy. I've also tried changing the percent of the snapping (10%, 80%, 50%, 30%), but these look jumpy after a short time (since the error keeps increasing, the snap distance increases as well).

I've read that you are supposed to interpolate the current position with the position from reconciliation. But what are you supposed to interpolate between? Prediction occurs every frame, so there's no visual time to interpolate the position unless I send the entity back in rendering time, but that defeats the purpose of client-side prediction. Am I approaching this wrong?

\$\endgroup\$
  • \$\begingroup\$ Did you ever get any further with this? Any chance your code is Open Source? \$\endgroup\$ – pookie Jul 19 '16 at 16:00
  • \$\begingroup\$ @pookie Yes, I ended up fixing all my problems. I've written an answer to my question with a longer explanation than can fit in a comment. gamedev.stackexchange.com/a/125788/17200 \$\endgroup\$ – pjrader1 Jul 19 '16 at 20:23
5
\$\begingroup\$

I solved the problem on my own. My previous implementation of the networking engine made reconciliation impossible. The code I am using is private, though I plan on open sourcing the Box2D server/client-side prediction eventually. My solution is as follows:

  1. My game code is put into a separate custom Entity-Component-System, called NetEngine. The NetEngine only ticks with a fixed time step (the tick rate of my server, 33 ticks/second). Each tick increases a frame count, called frameNum, by 1.
  2. Inputs are collected by clients and put into an InputSnapshot, which is sent to the server. InputSnapshots are collected at the server's tick rate. Each snapshot has a frame number attached to it (representing the server's ticks since the game has started). The client also saves snapshots for reconciliation later.
  3. The NetEngine saves states of physics objects it controls (server = all entities, client = predicted entities) each frame. The frame number is attached to the snapshot. On the client, this is used for reconciliation, on the server it is used for lag compensation.
  4. The server ticks the NetEngine when all the InputSnapshots from all players on all clients have been received, and sends the client updates on entities. The frame number is attached for reconciliation and interpolation. (ticking the engine is as follows: First, set the inputs of all entities to the snapshots. Second, tick the NetEngine. Third, send clients entity updates. Repeat this process until the NetEngine can no longer tick, aka when there aren't enough InputSnapshots).
  5. Clients save PhysicsSnapshot for reconciliation.
  6. When a PhysicsSnapshot for the player's entity is received, check the player's saved InputSnapshots. If the earliest saved InputSnapshot has a corresponding PhysicsSnapshot (by frame number), we can begin reconciliation.
  7. Go through all entities and apply PhysicsSnapshots for the frame number found. Then, apply InputSnapshots for the player and tick the NetEngine. Keep applying InputSnapshots (and PhysicsSnapshots for entities that are not predicted) and ticking the NetEngine until the frame number has reached the current predicted frame number.
  8. Remove the InputSnapshot used for that round of reconciliation.
  9. Repeat the reconciliation process until no more reconciliation can be performed.

The client updates the NetEngine on its own (once every two frames, about, since my server runs about half as slow as my clients). In the client's Box2D simulation, all entities are kinematic except for predicted entities, which are dynamic. This allows the client to predict future states of its players, and check them again using the server's authoritative positions.

From what I have noticed, when this is implemented Box2D is fairly deterministic. I wouldn't bet on this, though, which is why I still run reconciliation.

It should also be noted that with the current setup (assuming interpolation is used), everything will render at 60fps except for the client's predicted players, which render at 33fps (the tick rate of the server). The fix for this, however, is to predict the next frame, and interpolate between the two frames. This fixes the client's players rendering at 33fps, and everything is smooth.

I did not describe lag compensation (I haven't implemented it myself), however there are many articles describing how. Essentially, the server keeps track of all PhysicsSnapshots and InputSnapshots, and because of this can rewind the state of the world in such a way that the server can recreate whichever client it chooses at any given time. The server is then able to determine if certain events have taken place (such as a player shooting another player, for example).

I apologize for the messy explanation, it's hard to describe server reconciliation in such a short response. However, if anybody has any questions regarding my method I'll be happy to answer them to the best of my ability.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.