How does it work? - thomaswp/BeaverBuddies GitHub Wiki

How does BeaverBuddies work?

This page lays out the principles that allows BeaverBuddies to work. It is useful for developers aiming to contribute to the project.

Multiplayer Strategy

The way that BeaverBuddies keeps two (or more?) games in sync is to ensure a few things:

  1. Start State: All players start in the exact same state.
  2. Mirrored Inputs: Any input made by any player is duplicated across all players' games, at the exact same time.
  3. Determinism: The game is completely deterministic, so that given the same inputs, the game state will progress exactly the same for all players.

If all players start in the same state, receive the exact same inputs, at the exact same time, and the game is fully deterministic, all players stay in the same state. The idea is simple. In practice, this can be quite difficult.

Start State

This is the easiest part. Both players must load the exact same save file.

However, this does produce a limitation (at least for now) with BeaverBuddies, described in this issue, where players cannot join after the game has started. Even if the mod creates a save at later point in time, the save does not fully capture the game state in memory, so players to not start in identical states.

The Connect namespace has code that handles the UI for connecting and transferring the map between players.

Mirrored Inputs

BeaverBuddies uses a Host/Client model, where there is a single Host, and one (or hopefully more) Clients. All inputs are send to the Host, which then broadcasts them out to the Client(s).

To do this successfully, Beaver Buddies, has to do the following:

  1. Capturing All User Events: Any user input to the game, big or small (e.g. building a building, changing a building's recipe), has to be intercepted. Importantly, that game event must not happen when the user initiates it. Instead, the event is captured, and sent to the Host to be played. Even the Host intercepts user events and records them to be replayed later.
  2. Broadcasting & Playing Events: Once per tick, the Host plays (i.e., actually executes) any queued events and broadcasts them out to Clients. This creates a small amount of lag for the Host, and possibly larger lag to for the Clients (since events have to first go to the Host, and then get broadcast back to all Clients).
  3. Timing Events: The Clients must time their execution of events to perfectly match the Host. To do so, Clients only progress the game by a tick when they have received events from the Host for that tick. This ensures that the Clients never get ahead of the Host, and that all events happen at the exact same Tick for all parties. To ensure that Clients don't get too far behind the Host, the Host sends out a "Heartbeat" event every tick, even if there are no user-initiated events.

Example:

  • The Client clicks to build a building on Tick 32. The event is recorded and sent to the Host, but not executed (i.e., no building is built).
  • The Client waits to receive events for Tick 33.
  • The Host sends a Heartbeat out for Tick 33, and the Client progresses 1 tick, but does not do any events.
  • The Host receives the build event and executes it on Tick 34 (the Host sees the building built).
  • The Host then broadcasts the event out to all Clients.
  • The Client waits to receive events for Tick 34.
  • The Client receives the build event and executes it on Tick 34.

The TimberNet project contains the networking code for sending the events between Host (Server) and Clients.

The Events namespace holds events that can occur, how they're intercepted, and replayed.

Both Client and Host are on Tick 34, when the building is built. There was 2 ticks (~0.66 seconds) of delay for the Client.

Determinism

To ensure the game behaves fully deterministically (no variation or randomness across machines), BeaverBuddies has to make two important changes:

  1. Only game logic can use UnityEngine.Random (or Timberborn's RandomNumberGenerator). Any other randomness (e.g., music, sounds, animations, user interface) must use a different random number generator. Otherwise, when music plays for one player, it will change the random state and desync. Therefore BeaverBuddies must intercept a lot of random calls in non-game logic and reroute them.
  2. Nothing can depend on the framerate (i.e. Time.deltaTime or Time.time). To address this BeaverBuddies, overrides these properties and returns a deterministic number based on the number of ticks that have passed. This breaks other game logic, such as animations, which have to be reconstructed (e.g. here).

The logic for maintaining determinism is in the DeterminismService class.