schteppe / p2.js

JavaScript 2D physics library
Other
2.64k stars 330 forks source link

What and how much state is there? #246

Closed dasilvacontin closed 8 years ago

dasilvacontin commented 8 years ago

Hi!

I'm building a multiplayer game using p2. Every Turn is generated using the previous Turn + the GameEvents received for the previous Turn. My bodies have fixed rotation, and in each Turn I store the position, velocity and angle properties of each body.

The simulation using p2 runs both on the clients and on the server, so that – simplifying – the server only has to broadcast events and the turn index they should be applied to. If you get a GameEvent for a previous turn, you apply the event and re-simulate the game from that Turn.

For this to work, the evolve function of the Turns should be deterministic. However, I'm afraid that I'm not storing enough of p2's state and that (worse even for me) p2 relies on storing state for the past step.

Also, I noticed that the order of the bodies in world's bodies array matters, but I'm already handling that. I have issues with simple stuff as applying forces – it sometimes moves more in one simulation than in the other.

How much state p2 relies on in order to step the world? In Turn::evolve I need to be able to reset the bodies (since they are shared to save up memory), and – using the Turn's info – restore the relevant/important properties to correctly and deterministically step the world.

Thanks for the library and the help! :)

dasilvacontin commented 8 years ago

I could make the server correct the clients every now and then, but it's sad since ideally it should work without correction if it were deterministic.

schteppe commented 8 years ago

Hello! p2.js is deterministic. If you do the same things with your physics world on the same physics tick, everything will be the OK. For example, this would produce the same state B everywhere, assuming state A is the same:

// --- State A ---
world.addBody(body);
world.step(1/60);
body.applyForce(...);
world.step(1/60);
// --- State B ---

If you update your physics in a render loop, you are in a tricky situation. You have to make sure that the number of physics ticks are the same and that the .applyForce() are run in the exact same tick (world.time must be equal).. Not sure how to do that, if you want a continuously running simulation on both server + client.

"Synchronizing everything" is a bit of work. p2 was not really designed to do this, and it's probably very complicated to do in any physics engine. You have to synchronize the ticks (as mentioned), all bodies, all contacts, all constraints, all springs, the solver data, the overlapKeeper data (if you use the contact events), the order of all the world objects, the order of the attached event listeners, the order of bodies+shapes+etc... Perhaps also the id's of the shapes+bodies, I'm not sure.

Is it not possible to just re-create the World instance (and everything in it) and simulate it a while for each turn? Like,

// --- State A ---
var world = createWorldFromState(turnState);
// ...simulate the world for 100 ticks in a render loop:
// if(world.time < 100*fixedDeltaTime) world.step(fixedDeltaTime); else endLoop();
var newTurnState = getStateFromWorld(world);
// --- State B ---

This would probably be the easiest approach. And the turnState above would not have to contain much at all, maybe only a vector3 if a position is all you're interested in for your game. Storing everything could potentially be a lot of data (for example, you never know how many contacts that can be generated during a turn).

Not sure if this is the reply you expected, but I'd really recommend re-creating the physics world instead of synchronizing everything: the amount of work is greatly reduced, and unexpected state drift can be completely avoided this way.

dasilvacontin commented 8 years ago

Hi @schteppe! Thanks, and good to hear!

I'm already handling tick synchronisation. I synchronize the game's start timestamp, and I step the logic/physics/p2 TIME_STEP ms for every TIME_STEP ms that have passed since the game's start.

I tried the re-create the World instance solution. I don't remember if it worked, because the game would slow down / freeze after a few seconds – probably due to the garbage collector after creating and dumping too many js objects (worlds and bodies)...

Is there a method to reset and reuse a p2.World or p2.Body? To make an object pool, or some deterministic instance reuse system. Otherwise I could try creating my own. At least I now know that this is probably the best solution / direction to take. :)

schteppe commented 8 years ago

All right cool! Freeze after a few seconds? That must be a lot of garbage. How often do you have to sync / rebuild? If it's necessary to rebuild more than once per seconds or so, then I understand, and reusing the objects is probably the way to go.

(You should in any case look for memory leaks. Don't keep references to things that should be garbage collected etc).

What kind of game is this? It's not turn based and not realtime multiplayer, maybe something in between?

You may be interested in how Unreal does their sync (though it's just approximated client side prediction and having the server authoritative): https://udn.epicgames.com/Three/NetworkingOverview.html#Physics

There's no built-in way to reuse the objects.. you'll have to clean them up yourself. Check the source and go through all properties.

dasilvacontin commented 8 years ago

@schteppe it's a racing game: http://ag-drift.herokuapp.com/ (left-arrow/right-arrow to turn 90 degrees, up-arrow for main thrust, A/D to lean to the sides, S to boost)

The ghost is the server's simulation, for comparison with your local one.

The game will have to re-simulate every time it gets an event meant for a past turn... which will be always, since TIME_STEP is 16ms right now.

I'll take a look at the link :) Sounds like my plan B: only the server does the real simulation and it corrects the clients every now and then. Still, my physics step is very short (small map, not many bodies, all rectangles), so there should be no problem in re-simulating 30 turns in a go on the client, for correction.

There's no built-in way to reuse the objects.. you'll have to clean them up yourself. Check the source and go through all properties.

I'll try that. :)

dasilvacontin commented 8 years ago

There must be something wrong with my code, because I'm now creating new instances of World and Bodies for each Turn simulation, and it still doesn't sync correctly.

I'll keep digging / debugging.

dasilvacontin commented 8 years ago

But my doubts are solved – thanks much! :)

dasilvacontin commented 8 years ago

Found an important bug :) (kinda typo / dislexia)

I wasn't applying events to the desired Turn, but instead to the current one, always. https://github.com/dasilvacontin/ag-drift/commit/e0bdf8c01b8a54f05024f7272965f48224f5426b

Lesson: use good variable names that describe the content. i.e this.currentTurn instead of this.turn. Reminds me of GitHub when it asks you to write the name of the repo you are deleting.

dasilvacontin commented 8 years ago

@schteppe Are Shapes and/or Materials ever mutated by stepping the World?