How two Cauldron2D.Worlds, one on each machine, play one game with rollback: each
runs the game on its own clock, applies its own players' input at once, and runs
frames again when the other's input arrives late.
Every join, leave and input is an action stamped with the frame it applies before.
A world stamps its own players' input delay frames ahead of the frame it is on, and
sends it to its peer once. The host also stamps every join and leave, its own and those
the other world asks for. Each message also says up to which frame the sender has sent
every action of its own (through) and which frame it is on.
A frame is run with the actions known for it. A player whose input for a frame is not
yet known keeps their last input, so held buttons stay held and none is pressed or let
go. When an action arrives for a frame already run, the world loads the state it kept
from before that frame and runs the frames again. The game's events from frames run
again are dropped. A world runs no further than window frames past its peer's
through (advance?/1).
Each world measures how far ahead of its peer it runs: its frame less the peer's last
reported frame, once for every frame it sends. Every 250 milliseconds it averages those
and sends the average. When its own average less the peer's is 1.5 frames or more, it
owes 0.3 of that many waits, at most window, and skips one step in every three until
it has waited them (wait/1). The world behind never hurries.
Each world sends the checksum (Cauldron2D.Game.checksum/1) of the state before every
60th frame once both worlds have confirmed it. A checksum that differs is counted in
desyncs and nothing else is done.
delay/2 picks the delay from the round trip between the two worlds, and delayed/2
changes it while the worlds play: input stamped further ahead leaves frames with no
action, which keep the last input, and input stamped nearer comes after the actions
already sent for its frame, in the same order on both worlds.
Summary
Functions
Whether world may run its next frame: no more than window frames past what its peer confirmed.
world after the peer's checksum sum of the state before frame.
The delay in frames for a round trip of round_trip_ms between the two worlds at
hz frames a second: half the round trip and 8 milliseconds, in whole frames, from 2
to 3.
world with its own players' input stamped delay frames ahead from now on.
world playing on alone: every action it holds for a frame not yet run applied now,
in frame order, and its rollback part gone.
world after sending its peer the actions stamped since the last message, and its
average lead when one is due.
world after the peer's average lead.
world with action by one of its own players stamped and kept to send.
The rollback part of a world playing against peer. host? says whether this world
stamps joins and leaves. Options: :delay (frames, default 2) and :window (frames,
default 7).
world after the peer's actions, through and frame: the actions kept, and the
frames already run with an action now known for them run again.
The game state exported stands for, as Cauldron2D.Game.restore/1 makes it.
world after running its next frame: the state kept, the frame's actions applied, one
step of the game run.
Whether world has heard nothing from its peer for 7 seconds.
What the host sends the other world when it starts: the frame the host is on, its state, and every action it holds from that frame on.
The world's rollback counts: how many times it went back, how many frames it ran again, and how many checksums differed from its peer's.
Whether world skips this step to let its peer catch up, and world with the wait
counted. It skips one step in every three while it owes a wait.
Types
@type t() :: %{ peer: pid(), monitor: reference(), host?: boolean(), delay: non_neg_integer(), window: pos_integer(), frames: %{required(non_neg_integer()) => [action()]}, states: %{required(non_neg_integer()) => term()}, through: non_neg_integer(), sent: [{non_neg_integer(), action()}], heard_at: integer(), peer_frame: non_neg_integer() | nil, samples: [integer()], next_average_at: integer(), own_average: number() | nil, peer_average: number() | nil, to_wait: number(), gap: non_neg_integer(), sums: %{required(non_neg_integer()) => term()}, peer_sums: %{required(non_neg_integer()) => term()}, summed: integer(), desyncs: non_neg_integer(), rollbacks: non_neg_integer(), rolled_frames: non_neg_integer() }
Functions
Whether world may run its next frame: no more than window frames past what its peer confirmed.
@spec checked(map(), non_neg_integer(), term()) :: map()
world after the peer's checksum sum of the state before frame.
@spec delay(number(), number()) :: pos_integer()
The delay in frames for a round trip of round_trip_ms between the two worlds at
hz frames a second: half the round trip and 8 milliseconds, in whole frames, from 2
to 3.
@spec delayed(map(), pos_integer()) :: map()
world with its own players' input stamped delay frames ahead from now on.
world playing on alone: every action it holds for a frame not yet run applied now,
in frame order, and its rollback part gone.
world after sending its peer the actions stamped since the last message, and its
average lead when one is due.
world after the peer's average lead.
world with action by one of its own players stamped and kept to send.
The rollback part of a world playing against peer. host? says whether this world
stamps joins and leaves. Options: :delay (frames, default 2) and :window (frames,
default 7).
@spec received( map(), [{non_neg_integer(), action()}], non_neg_integer(), non_neg_integer() ) :: map()
world after the peer's actions, through and frame: the actions kept, and the
frames already run with an action now known for them run again.
The game state exported stands for, as Cauldron2D.Game.restore/1 makes it.
world after running its next frame: the state kept, the frame's actions applied, one
step of the game run.
Whether world has heard nothing from its peer for 7 seconds.
@spec start(map()) :: %{frame: non_neg_integer(), state: term(), frames: map()}
What the host sends the other world when it starts: the frame the host is on, its state, and every action it holds from that frame on.
@spec stats(map()) :: %{ rollbacks: non_neg_integer(), frames: non_neg_integer(), desyncs: non_neg_integer() }
The world's rollback counts: how many times it went back, how many frames it ran again, and how many checksums differed from its peer's.
Whether world skips this step to let its peer catch up, and world with the wait
counted. It skips one step in every three while it owes a wait.