Skip to content

Latest commit

 

History

117 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lockstep-go

A lightweight lockstep networking server written in Go. It uses WebSocket for signaling (room management, RPC) and KCP over UDP for real-time frame synchronization, making it suitable for deterministic multiplayer games.

Architecture

┌────────┐  WebSocket (TCP)  ┌──────────┐  KCP (UDP)   ┌────────┐
│ Client ├──────────────────►│  Server  │◄────────────►│ Client │
└────────┘  signaling/RPC    └──────────┘  frame sync  └────────┘
  • WebSocket layer – handles player connection, room create/join/leave RPCs, and peer-to-peer message relay.
  • KCP/UDP layer – each room opens a dedicated UDP port and runs a KCP session per player for reliable, low-latency frame delivery.

Room lifecycle

  1. A player calls create_room → server allocates a UDP port, creates a Room, and starts its main loop.
  2. Other players call join_room (with pre_join for a two-phase join) → the room assigns each player a unique conv (KCP conversation ID).
  3. The room ticks at the configured frame rate. Each tick it collects player inputs received via KCP, packs them into a frame, and broadcasts the frame to all players.
  4. When all players leave, the room shuts down and releases the UDP port.

Requirements

  • Go 1.25+

Build

go build -trimpath -ldflags "-s -w" -o lockstep-go-server ./cmd/

Configuration

The server is configured via constants in cmd/main.go:

Parameter Default Description
wsPort 5999 WebSocket listening port
udpPortMin 6000 Start of the UDP port range for rooms
udpPortMax 8000 End of the UDP port range for rooms

udpPortMin and udpPortMax are derived from wsPort in code (wsPort + 1 and udpPortMin + 2000).

Room-level constants are in pkg/core/config.go:

Constant Value Description
MAX_PLAYERS_PER_ROOM 8 Maximum players per room
MAX_INPUTS_PER_PLAYER_PER_FRAME 10 Input cap per player per frame
IKCP_MTU_SIZE 900 KCP MTU size
PingInterval 20s WebSocket ping interval
PendingPlayerTimeout 10s Timeout for a pre-joined player to complete join

Protocol

WebSocket (Signaling & RPC)

All WebSocket messages are MsgPack-encoded arrays of 4 elements:

[srcID, dstID, method, arg]
  • srcID – sender's player ID (the TCP remote address).
  • dstID – receiver's player ID. If non-empty, the server relays the message to that player (peer-to-peer relay). If empty, the server handles it as an RPC call.
  • method – RPC method name. Prefix with ! for fire-and-forget (no reply).
  • arg – method argument (any MsgPack-encodable value).

RPC response format:

["", srcID, "_", [retval, error]]

Available RPCs

Method Arg Returns Description
create_room {version, max_players, frame_rate} Room object Create a new room
join_room {port, version, pre_join} Room object Join an existing room
leave_room Leave the current room
room_kcp_ready Mark current player's KCP channel as ready

Server push notifications (no-reply)

Method Arg Description
push_player_id player ID string Sent on connect to inform client of its ID
push_room_event {type, player} Notifies room members of room events (pre_join, join, leave, join_timeout, kcp_ready)

KCP/UDP (Frame Sync)

KCP messages are MsgPack-encoded arrays:

[opcode, arg]

Opcodes

Code Name Direction Arg Description
0 Ping Client ↔ Server Keepalive ping
1 Pong Server → Client Keepalive pong
2 ServerInput Client → Server raw bytes or any Player input for current frame
3 ClientFrame Server → Client {id, inputs} Broadcast frame with all player inputs

Frame format (ClientFrame arg)

{
  "id": 42,
  "inputs": {
    "<conv>": [input1, input2, ...]
  }
}
  • id – monotonically increasing frame sequence number.
  • inputs – a map from player conv (KCP conversation ID) to an array of inputs submitted during this frame.

Lockstep flow

Client A                Server                Client B
   │                       │                       │
   ├──[ServerInput]───────►│                       │
   │                       │◄──[ServerInput]───────┤
   │                       │                       │
   │        (frame tick)   │                       │
   │                       │                       │
   │◄──[ClientFrame]───────┤───[ClientFrame]──────►│
   │   {id:1, inputs:{…}}  │   {id:1, inputs:{…}}  │

All clients receive the same frame data in the same order, ensuring deterministic simulation when combined with a deterministic game engine.

License

MIT

About

Lockstep Multiplayer Framework

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Contributors

Languages