Why Am I here?
You were sent here from issues. Its important that you implement code that is deterministic for multiplayer, and this is an overview to get you up to speed.
How does our Multiplayer Work?
We are using Deterministic Lockstep which mean we keep simulations in sync by sending only inputs.
We might outgrow lockstep but currently its perfect for a client-to-client connection between two players.
When a multiplayer game occurs, one player generates a seed eg. picardvsjaneway
, its sent to the other player, and then both games are generate based off of that seed.
We feed that seed into seedrandom which allows us to have a predictable random number generator.
When the game is running we take snapshots of object data on each frame for upto a 120 frames at any given time.
When games go out of sync, we will load an old snapshot, and then replay inputs until they are both back in sync.
How do I implement things to Deterministic?
Game objects will have snap
, load
, render
and update
functions eg.
class ComponentPanel {
get snap() {
return [this.kind, this.x, this.y]
}
load(snapshot) {
this.kind = snapshot[0]
this.x = snapshot[1]
this.y = snapshot[2]
}
update() {
this.kind = //assign based on some logic
this.x += 2;
thix.y += 2;
}
render() {
this.sprite.x = x
this.sprite.y = y
}
}
update
runs at a fixed framerate, meaning that every 60frames = 1 second.render
can run as many times as it likes.snap
is used to snapshot game object data needed to reload laterload
is used when want to load a snapshot to restore a previous state of a game object.
When render
is called we should already determined that game logic. Game logic should never change inside the render
method. Because it doesn't run at a fixed framerate, and it has to be work regardless of state.
This is bad:
render() {
this.sprite.x += 1
}
This is good:
render() {
this.sprite.x += 1
}
Gotchas
One of trickiest things is how do you ensure you can go back to any frame and ensure the animations are perfectly at the correct frame.
In Phaser you would be tempted to use the built in phaser animations:
update() {
this.character.sprite.play("losing");
}
However this is not deterministic because you have to be able to go back to any frame. So in this case you need to have a variable such as a counter
that you will snapshot, and use that counter as the way to manage which frame you are on.
class Character {
snap() {
return [this.counter]
}
load(snapshot) {
this.counter = snapshot[0]
}
update() {
this.counter++
}
render() {
this.sprite.frame = this.counter
}
}