A usual game loop works like this:
This works well in a realtime single player games.
Multiplayer games must all consider many "viewers" of the game state, and multiple input sources that contributes to the update of the shared game state.
A common approach is to handle local input and state changes locally, send the input to a central source of truth and adjust the game state according to the global state if it diverges from the local state. This accounts for the network delay and makes a game playable even with slow connections.
On the other hand, turn based games are a bit less demanding in terms of realtime updates.
A game loop can be changed to:
This approach may seem costly in a usual game with a 120Hz refresh rate but it proved to work quite nicely for less demanding games.
One thing to consider is that the game state and view (UI) state are totally independent so the UI rendering code can be as complicated and costly as you like, creating rich eye candy interfaces on the client side.
Actions and events are best represented as pure data, especially if we consider that this information should be exchanged over a network.
Example actions:
class Action(Enum):
JoinGame = 0
RollDice = 1
[Action.JoinGame, {'name': 'foo'}]
[Action.RollDice, {'die_ids': [2, 3]}]
# Base action handler
action = await receive_action() # for example [Action.JoinGame, {'name': 'foo'}]
events = match action:
case [Action.JoinGame, opts]:
generate_join_game_events(opts)
case [Action.RollDice, opts]:
generate_roll_dice_events(opts)
send_events(events)
Or events:
class Event(Enum):
PlayerJoined = 0
DiceRolled = 1
GameOver = 2
[Event.PlayerJoined, {'client_id': '123', 'name': 'foo'}]
[Event.DiceRolled, {'faces': {'die_2': 4, 'die_3': 6}}]
[Event.GameOver {'winner': '123'}]
# Base events handler
for event in await receive_events():
match event:
[Event.PlayerJoined, opts]:
handle_player_joined(opts)
So to recap:
At this point we have a kind of protocol based on Actions and Events to drive game and UI state updates; it works nicely for a local game but (not) suprisingly, being data based (something that can be de/serialized), it work over a network connection as well.
To break it down we have clients and servers.
Clients
Servers
All we need at this point is a central component that coordinates the exchange of the messages, at the bare minimum it should offer: