-
Notifications
You must be signed in to change notification settings - Fork 0
Protocol spec
See #2 for discussion.
All communication is done via discrete messages, each having a type and zero or more key-value properties. Three definitions below: protocol in abstract terms, encoding of the messages (currently JSON) and transport/framing (currently TCP/WebSocket). Ping/pong and timeouts are handled in the transport, so there shouldn't be any messages doing that in the protocol.
I'm including some data bits that aren't necessarily needed, but will make tuning the game logic easier (because we won't have to change the client as well as the server), this can go but it shouldn't be too much of an issue — all of those things will be constants to begin with.
All speed/position values are in the same scale. Distance unit is the same as in HTML5 canvas, i.e. pixels. Time unit is a second. Player vehicles are assumed to be squares, and the size is the square's side. Player bullets are assumed to be circles, and the size is the circle's radius.
I've used Rust names for types.
Terms like MUST, SHOULD etc as in RFC2119 as per usual.
welcome — sent by the server to a client, after the client successfully connects (what that means is defined by the transport) — all data values apply to all players and are constant
-
id
(u32) — server-assigned ID of the player, MUST NOT change during the connection -
speed
(f32) — speed of movement of player ships -
size
(f32) — size of the player vehicle -
bullet_speed
(f32) — speed of movement of player bullets -
bullet_size
(f32) — size of the player bullets
go_away — sent by the server if it rejects/terminates client connection for any reason
-
reason
(str) — a message to be displayed to the user
player_joined — sent by the server to all connected clients when a new player joins the game.
-
id
(u32) — server-assigned ID of the player
player_left — sent by the server to all connected clients when a player disconnects
-
id
(u32) — ID of the player that just left; server MAY recycle this ID, and client MUST be ready for that
shots_fired — sent by the server to all connected clients when a player fires a bullet (I'm giving bullets their own ID to make them easier to despawn but honestly not sure if that's the best of ideas)
-
id
(u32) — ID of the shooting player -
bullet_id
(u32) — ID of the bullet; server MAY recycle this ID, and client MUST be ready for that -
x
(f32) — position X of the player at the moment of firing (centre) -
y
(f32) — position Y of the player at the moment of firing (centre) -
aim_x
(f32) — player's aiming direction vector X at the moment of firing -
aim_y
(f32) — player's aiming direction vector Y at the moment of firing (aiming direction vector MUST be normalised, i.e. its magnitude MUST be equal to 1)
player_spawned — sent by the server to all connected clients when a player (re)spawns on the map
-
id
(u32) — ID of the player -
x
(f32) — position X of the player vehicle (centre) -
y
(f32) — position Y of the player vehicle (centre)
player_destroyed — sent by the server to all connected clients when a player despawns from the map
-
id
(u32) — ID of the player -
killer_id
(Option<u32>) — ID of the killer, if any -
bullet_id
(Option<u32>) — ID of the bullet, if any; MUST be present ifkiller_id
is present
player_moving — sent by the server to all connected clients when a player starts moving or changes the movement direction
-
id
(u32) — ID of the player -
x
(f32) — position X of the player when they started to move (centre) -
y
(f32) — position Y of the player when they started to move (centre) -
move_x
(f32) — player's movement direction vector X -
move_y
(f32) — player's movement direction vector Y (movement direction vector MUST be normalised, i.e. its magnitude MUST be equal to 1)
player_stopped — sent by the server to all connected clients when a player stops moving completely
-
id
(u32) — ID of the player -
x
(f32) — final position X of the player (centre) -
y
(f32) — final position Y of the player (centre)
world_state — full update of the world, sent by the server to all connected clients periodically (interval up to the implementation)
-
player_count
(u32) — count of all connected players -
alive_players
(Player[]) — an array of all currently alive players, each containing:-
id
(u32) — ID of the player -
x
(f32) — current position X of the player -
y
(f32) — current position Y of the player -
move_x
(Optional<f32>) — current movement direction vector X of the player, if the player is moving -
move_y
(Optional<f32>) — current movement direction vector Y of the player, if the player is moving
-
-
alive_bullets
(Bullet[]) — an array of all currently alive bullets, each containing:-
id
(u32) — ID of the bullet -
x
(f32) — current position X of the bullet -
y
(f32) — current position Y of the bullet -
move_x
(f32) — current movement direction vector X of the bullet -
move_y
(f32) — current movement direction vector Y of the bullet (movement direction vectors MUST be normalised, i.e. their magnitude MUST be equal to 1)
-
start_moving — sent by the client to the server when the player wants to start moving or change its movement direction (i.e. presses/releases one or more movement keys, as long as at least one of them is still held)
-
move_x
(f32) — player's movement direction vector X -
move_y
(f32) — player's movement direction vector Y (movement direction vector SHOULD be normalised, but the server MUST NOT assume that it is)
stop_moving — sent by the client to the server when the player wants to stop moving (i.e. releases all held movement keys)
fire — sent by the client to the server when the player wants to fire (i.e. presses the mouse button)
-
move_x
(f32) — player's aiming direction vector X -
move_y
(f32) — player's aiming direction vector Y (aiming direction vector SHOULD be normalised, but the server MUST NOT assume that it is)
To start with, we encode all messages as JSON objects, with the type ID being stored in type
key, and message properties being stored as another object in data
key (and that object has key per property), e.g.
{
"type": "world_state",
"data": {
"player_count": 32,
"alive_players": [
{ "id": 1, "x": 34.66, "y": 21.44 },
{ "id": 6, "x": 67.34, "y": 22.22 }
]
}
}
The data
key MAY be omitted if the message doesn't define any properties. Optional properties MUST be omitted if they're not present (and not set to null
).
Newlines and indenting added for example purposes: all the exchanged messages SHOULD NOT contain any unnecessary whitespace.
A message is malformed if it:
- contains unknown
type
value, or - doesn't contain any required fields, or
- contains values of types differing from the specification, or
- doesn't decode properly (or violates JSON specification in any other way)
All malformed messages MUST be rejected. Additionally, messages containing extra fields MAY be rejected (but the extra fields can also be ignored).
All messages are sent as WebSocket text frames (which also implies UTF-8 encoding for the serialised objects). Both client and server MUST properly handle Ping and Pong frames, and send them periodically to detect dead endpoints (refer to WebSocket specification for details).
Client/server SHOULD try to negotiate WebSocket compression, if possible.
The server MUST terminate any client connection that:
- fails to establish a proper WebSocket connection, or
- sends a binary frame, or
- sends a malformed frame (or violates WS protocol in any other way), or
- sends a frame that's too big (threshold up to the implementation), or
- sends a frame with a malformed message, or
- hasn't sent a Ping/Pong frame in time (threshold up to the implementation), or
When the server terminates a connection with the client that went through WebSocket handshake properly, it SHOULD send it a go_away
message before closing the socket.