liabru / matter-js

a 2D rigid body physics engine for the web ▲● ■
MIT License
16.63k stars 1.96k forks source link

How to make matter-js deterministic? #1190

Closed Quitalizner closed 1 year ago

Quitalizner commented 1 year ago

I'm running matter-js on a server and on the client side I'm using matter-js that is built into phaser.

I'm running both at fixed tick rate with similar configuration. Yet I see differences in outcomes. I came across this. But I don't exactly know how to implement.

I went through the code for both matter-js and phaser matter-js they both start with _seed set to 0. Yet the outcome seems to be different. Any advice?

Quitalizner commented 1 year ago

I guess matter-js is not very suitable for multiplayer games as stated here

reececomo commented 1 year ago

+1 Having the same issue. Had just ported an existing game's physics to MatterJS, and now the simulation appears to be unstable when replaying frames for server reconciliation. The game uses a fixed timestep.

It's possible there's an issue with the verlet integration <--> serializing/deserializing, or maybe some non-determinism in the trig functions, or relying heavily on low/lossy floating point numbers. @Quitalizner did you end up going with P2/Planck?

Quitalizner commented 1 year ago

@reececomo, my assumption is that it is mostly related to floating point numbers. I don't think Planck is deterministic either.

I too had trouble getting the server reconciliation to work. There were some random glitches. I'm using Phaser for the client side which comes with Matter built in. After some late night debug sessions and a few conversations with other devs I was able to solve reconciliation issues by upgrading to v0.18.0

But it is still non-deterministic. And not well equipped for multiplayer games where saving world state, rollbacks and reconciliation is required.

I'm sticking with matter for the project that I'm currently working on, as I've already made changes to make it work to a certain degree. I might switch to Rapier for future multiplayer projects.

reececomo commented 1 year ago

After some late night debug sessions and a few conversations with other devs I was able to solve reconciliation issues by upgrading to v0.18.0

@Quitalizner The late-night debug sessions is currently where we're at now - running 0.18.2 unfortunately.

We're also getting that wild bouncing circle issue described in #934. I think we'll also need to look for a more viable engine.

We're able to mask some of it with visual smoothing, but it's suprisingly severe when replaying just 6-10 frames (i.e. ~120ms latency on a 60Hz simulation, with input/jitter buffers).

Have been pretty disappointed by matter-js - we had zero divergence doing basic physics in house.

I might switch to Rapier for future multiplayer projects.

👍 +1 to that. I'll take a look as well. Suprised there isn't a viable mainstream option with a v1.0 yet.

Quitalizner commented 1 year ago

@reececomo,

The bouncing circle is bit of an "edge" case and does happen in box2D too some times. If you tinker around with chamfer value a bit you might be able to solve it (Atleast it worked for me) I haven't noticed that issue in a while.

We're able to mask some of it with visual smoothing, but it's suprisingly severe when replaying just 6-10 frames (i.e. ~120ms latency on a 60Hz simulation, with input/jitter buffers).

I have run around 18 frames worth of recon on the main player body. Sometimes it takes ~2ms to run the entire thing. I used to encounter the severe jumping issues too, but was able to resolve it. I'm curious to know your recon approach for the physics body. The issue might be there.

reececomo commented 1 year ago

I'm curious to know your recon approach for the physics body. The issue might be there.

Possibly! It's the typical state sync techniques described by Glenn Fieldler, Gabriel Gambetta, and Jared Cone.

Quick summary:

With low numbers of fast-forward frames (i.e. 20 - 60ms of lag) the divergence is small enough when using matter-js that we can cover it up with the visual smoothing, but even with just one player interacting with one circle object, the pops/corrections are gigantic with 90-120ms.

Fundamentally when we slow the simulation down, it appears to be inconsistent results in the replayed collision vectors, and the butterfly effect makes those diverances more chaotic when we fast-forward.

In a deterministic simulation, there will only be a correction/pop when:


It is possible that we're missing a critical body value in the serialization/deserialization, or we need to set the _seed - so if you have any idea of what else could be causing such big inconsistencies in collision responses in matter-js, please let me know! 😄 Happy to give it a try

reececomo commented 1 year ago

Just for reference - the player character doesn't have any obvious pops/corrections, just the ball Bodies.circle(...) object in the world. It will have these corrections even when bouncing against walls by itself, with no player input.

reececomo commented 1 year ago

Also worth mentioning network layer is WebRTC via Geckos.io 👍

Quitalizner commented 1 year ago

@reececomo,

Visual smoothing: Prevent obvious visual pops by capturing a visualErrorOffset and lerp/slerp it back to zero.

Yeah, I do this too. Since matter is not deterministic. This is inevitable. Once I've completely stepped through local player's inputs, I compare player's predicted state and the recon state and apply lerp similar to what you do.


Fundamentally when we slow the simulation down, it appears to be inconsistent results in the replayed collision vectors, and the butterfly effect makes those diverances more chaotic when we fast-forward.

Haha, did use the same term to describe my scenario with the other game devs, when debugging this issue.


or we need to set the _seed

You could add the _seed value returned once the physics step is complete in the backend to your payload and set it before running the recon. Maybe this could help. I haven't tested this. Let me know your findings

Quitalizner commented 1 year ago

Just for reference - the player character doesn't have any obvious pops/corrections, just the ball Bodies.circle(...) object in the world. It will have these corrections even when bouncing against walls by itself, with no player input.

I see that you are running the world.update() to step through during recon. I like to mention that this was the source of many of the glitches I encountered initially. https://github.com/liabru/matter-js/blob/master/src/core/Engine.js#L88 This is the engine update code. I've noticed that the 'pairs' are cached for performance reasons. This was the source of my problems.

Here's what I did:

I'd like to repeat again that using engine.pairs and detector were the source of most jitter during recon for me. Also I would like to mention I didn't reset timestamp of the engine. I use tick rates and there is always some mismatch between the frontend and the backend. I just run through all the recon steps without updating or incrementing the timestamp

Quitalizner commented 1 year ago

Also worth mentioning network layer is WebRTC via Geckos.io 👍

What a coincidence. I'm building the game using geckos.io as well. But I guess the newer versions are using node-datachannel instead of WebRTC

reececomo commented 1 year ago

This is the engine update code. I've noticed that the 'pairs' are cached for performance reasons. This was the source of my problems.

Here's what I did:

  • I copied most of the update code into my project, this I specifically use for recon or rollbacks
  • Created a new pairs instance
  • Created a new detector instance
  • In the recon update code replaced the engine pairs and detector with the pairs and detectors that were created for recon
  • Also the bodies that are sent into the recon update code are only the ones that need to be affected by recon. (In my case it would be the player, bullets, the platform colliders and the wall
  • I even reduced the number of velocity and positional iterations in the recon update code for performance reasons. You can tweak iterations count to your liking

You legend - nice spot! Could we even use an Engine.clear() on each iteration? (At first glance it looks like it does a similar thing). Thanks so much for sharing the solution here - much appreciated.

What a coincidence. I'm building the game using geckos.io as well. But I guess the newer versions are using node-datachannel instead of WebRTC

Haha, nice 👍 Maybe a collaboration is in our future! (p.s. Yes node-datachannel)

Are you using geckosio/typed-array-buffer-schema or anything for serialization?

Quitalizner commented 1 year ago

Glad to help!

Could we even use an Engine.clear() on each iteration?

So when you use Engine.clear() other issues creep up. When we hit Engine.clear() all active collisions are cleared too. So you will end up with the objects sinking through the world. This was also one of the primary reasons for creating a separate pairs and detector for recon


Haha, nice 👍 Maybe a collaboration is in our future! (p.s. Yes node-datachannel)

Haha, that would be amazing!


Are you using geckosio/typed-array-buffer-schema or anything for serialization?

I'm yet to add serialization, I was looking into adding either geckosio/typed-array-buffer-schema or colyseus/schema. I would probably go with typed-array-buffer-schema since colyseus/schema comes also with delta updates which might pose problems when used on UDP

reececomo commented 1 year ago

cc You might be interested → I ran into a tonne of little issues with type-array-buffer-schema → most importantly nested structures, but I created a fork of js-binary and added 32-bit floats (fork) (js-binary#14).

Seems to be working wonders - packet size reduced by 75-90% without any other optimizations 🙌 and only took about 30 minutes 🎉

If you try that or find something else better let me know!

Quitalizner commented 1 year ago

@reececomo, Thanks a lot for sharing. I'm about the start handling the serialization. This certainly helps.

Have you done any benchmarking? How does js-binary run compared to type-array-buffer-schema. How fast is the serialization and deserialization?

Since we are sending/receiving packets at high frequency, the speed of serialization and deserialization also matters. So keen on knowing what you found out

Quitalizner commented 1 year ago

@reececomo, what do you think of avsc?

reececomo commented 1 year ago

Glad to help! I haven't tried implementing avsc, but I read in a benchmark somewhere it's technically faster. I found js-binary to be pretty damn fast, and unbelievably easy to use.

It only took minutes to switch, and server state packets shrank from ~600 → down to ~100 bytes. After adding 32-bit float support our state update packets dropped to just ~60 bytes. That meant we could slightly increase our redundancy, and now the gameplay is buttery smooth even with moderately high packet loss or variable latency.

For reference our server send rate is 60Hz, although we've been experimenting with 30Hz for mobile.

If you try out avsc let me know! It might be worth switching if it's easy to implement

Quitalizner commented 1 year ago

Great! Those numbers look good. I also felt js-binary being somewhat easier to use than avsc.

In my case, client send rate is 60hz, whereas the server send rate is 20hz.

Maybe I'll try out avsc in the future projects