Networking

Networking is a high-level module to abstract network logic for your games.

It handles a session between one server and its clients, messages between them, and the replication of nodes and components from the server to every client.

To use this module, include the following line in your project file:

require engine.networking

Overview

A session has one server, the authority that decides the game’s state, and its clients. has_network() is true while a session exists, is_server() says which side this peer is - and is also true with no session at all, so a peer that must be a real server asks for both.

Four mechanisms carry the game across that session:

  • Replication moves state from the server to every client. A component class annotated [replicated] appears on each client as a read-only replica, and follows every change the server makes to it. A node carrying one arrives as a mirror of the server’s node, at its transform and active flag, and follows its name and parent. The transform and active flag follow only after set_transform_replication(node, true), an engine component only after set_net_replication. has_remote_authority(comp) tells a replica from the authority’s own instance.

  • Messages travel either way as [net_command] structs, once, at the reliability and on the channel the annotation names. Replication is for state that persists; a message is for a thing that happened.

  • Events report what the transport did: a client connected, a peer went away. They arrive through [net_event_handler].

  • Ownership names which connection drives a node. set_node_owner on the server labels it, is_node_owner on a client answers whether a node is controlled by this player.

To set how often the server writes records use set_replication_fps function. A client shows a mirror where its newest record put it, so the motion is as smooth as that rate; a mirror with a dynamic body at the scene root is driven to that record by its velocity instead. The default rate needs no attention until a game has a reason to change it.

The server runs dedicated.das, a client runs main.das, both in the project root. A window that hosts from the lobby runs dedicated.das in its own process and joins it through main.das like any other client. The project’s other scripts are shared by both sides: put [replicated] components and [net_command] structs there so that both know them.

Engine components

On a node that carries a [replicated] script component, the server can also replicate these engine components. The replication is opt-in: a type follows to the mirrors only after the server turns it on for that node. A node with only engine components is not mirrored.

  • set_net_replication turns on one type for one node.

  • [replicated(sync = Mesh)] on the user component turns on the listed engine components for every node that component joins.

Example:

[replicated(sync = (Transform, Mesh))]
class Ship : Component {
    color : float4 = float4(1)
}

// server: one node more
set_net_replication(node, type<Collider>, true)

These types can replicate:

Other engine components (Camera, UIFrame, …) never replicate. While a type is off, each client keeps its own component of that type and the authority’s stays local; set_net_replication(node, type<T>, false) turns a type off again.

Messages

A struct annotated [net_command(reliability=..., channel=...)] is a message. broadcast_net_command(cmd) sends it: from a client to the server; from the server to every client, to one connection or to a set of them.

A function annotated [net_command_handler] receives it: def on_cmd(cmd : Cmd) on a client, def on_cmd(cmd : Cmd; connection : ConnectionId) on the server, where connection is the sender.

Note

There can be multiple handlers for one command, their call order is deterministic but unspecified.

Struct annotated with [net_command] must have a reliability argument and names how the transport delivers the message:

  • RELIABLE_ORDERED - arrives, in send order; a lost packet holds the ones behind it on its channel.

  • RELIABLE_UNORDERED - arrives, in any order.

  • UNRELIABLE - may be lost, arrives in any order.

  • UNRELIABLE_SEQUENCED - may be lost; one older than the newest already delivered on its channel is dropped.

The other argument is channel: the transport channel the message is sent on, Default when absent. Order and sequence are kept only inside one channel. Between two channels there is no order. There are three channels one can specify for a specific net_command:

  • Default

  • Controls

  • Events

Default channel is also used internally to carry essential engine messages, such as the handshake and the net schema, which are RELIABLE_ORDERED. The replication frames (RELIABLE_ORDERED) and the time sync use their own channels. The names do not limit the direction or the content; the engine treats the three channels the same way.

Note

RELIABLE_UNORDERED messages are sent via rotating internal channels, so specifying channel parameter with reliability=RELIABLE_UNORDERED doesn’t have any effect on which channel the message is going to travel.

How channel processing performs depends on the reliability of the messages:

  • RELIABLE_ORDERED: when a packet is lost, the later reliable packets on the same channel wait until it is resent. Packets on other channels do not wait.

  • UNRELIABLE_SEQUENCED: each packet waits for the reliable packets that were sent before it on the same channel. So a lost reliable packet also delays unreliable packets on that channel.

  • RELIABLE_UNORDERED, UNRELIABLE: nothing on the channel delays them.

Considering all the above, one can use:

  • Default channel, when sending messages whose relative order is important. This channel already carries engine’s internal RELIABLE_ORDERED, so posting UNRELIABLE_SEQUENCED messages on this channel could stagger messages delivery.

  • Controls channel, when sending messages every frame as UNRELIABLE_SEQUENCED (e.g. input), with no reliable messages on it. Otherwise each lost reliable packet on that channel delays them.

  • Events channel, when sending other messages, which must not wait for a lost packet on Default.

These guidelines are mere recommendations (except Default channel, misuse of which could slightly impact the game’s performance) and channels could be used differently based on programmer’s intentions.

Example:

[net_command(reliability=UNRELIABLE_SEQUENCED, channel=Controls)]
struct ShipInput {
    thrust : float
    turn : float
}

// client
broadcast_net_command(ShipInput(thrust = 1.0, turn = 0.0))

// server
[net_command_handler]
def on_ship_input(cmd : ShipInput; connection : ConnectionId) {
    print("connection {*connection} thrust={cmd.thrust}")
}

Events

A function annotated [net_event_handler(<event>)] receives a transport event, and must carry the signature that event names.

Note

Several handlers may listen for one event as well and their order is not guaranteed.

For a server:

  • client_connected - a client connected. Signature: (connectionId : ConnectionId; userId : uint64; name : string)

  • client_disconnected - a client disconnected. Signature: (connectionId : ConnectionId; cause : DisconnectCause)

For a client:

  • connected_to_server - the client connected to a server. Signature: () (no arguments)

  • disconnected_from_server - the client disconnected from a server. Signature: (cause : DisconnectCause)

Example:

// client
[net_event_handler(disconnected_from_server)]
def on_disconnect(cause : DisconnectCause) {
    print("Was disconnected, reason: {cause}")
}

// server
[net_event_handler(client_connected)]
def on_connect(connectionId : ConnectionId; userId : uint64; name : string) {
    print("Client connected conn={connectionId} id={userId} name={name}")
}

Synced time

In a session, the server and its clients read one clock:

  • get_sync_time - the time in seconds synced between the server and the client. Use it to stamp a message with the time of the thing that happened, so the receiver can compare it with its own get_sync_time().

  • get_fixed_tick - the number of the fixed step that on_fixed_update runs. The tick comes from the synced clock, so tick 100 on a client and tick 100 on the server are the same moment of the game. A client puts the tick in each input message it sends. The server then knows the step for which the client made that input, although the message arrives some ticks later.

Example:

[net_command(reliability=UNRELIABLE_SEQUENCED, channel=Controls)]
struct ShipInput {
    tick : int
    thrust : float
}

// client, in on_fixed_update
broadcast_net_command(ShipInput(tick = get_fixed_tick(), thrust = 1.0))

Constants

INVALID_CONNECTION_ID = reinterpret<connection_id::ConnectionId> -1

No connection: what get_node_owner answers for a node nobody drives, and for a node that does not exist.

LOCAL_PLAYER = reinterpret<connection_id::ConnectionId> private_network::LOCAL_PLAYER_CONNECTION_ID

This process’s player: the owner of a node the host drives itself; on a client, get_node_owner answers it for the client’s own nodes.

NET_MAX_PLAYERS = 127

The most players a session hosted from this game accepts.

Enumerations

DisconnectCause

Why a connection ended. A client reads it from NetDisconnectedFromServerCommand, a server from NetClientDisconnectedCommand.

Values:
  • ConnectionLost = 0x0 - timed out

  • ConnectionClosed = 0x1 - the other side closed it

  • ConnectionAttemptFailed = 0x2 - the dial never reached a server

  • ConnectionStopped = 0x3 - the other side stopped, typically on a fatal error

  • NetProtoMismatch = 0x4 - the peers register different replicated types or net commands

  • ServerFull = 0x5 - no free incoming connections

  • KickGeneric = 0x6 - the game kicked the player

  • KickInactivity = 0x7 - the server saw no packet for its inactivity timeout

  • KickAnticheat = 0x8 - the anticheat kicked the player

  • KickFriendlyFire = 0x9 - kicked for friendly fire

  • KickVote = 0xa - kicked by a vote

  • Complaints = 0xb - kicked for too many complaints

  • ServerNotReady = 0xc - the server is still starting up

Structures

LobbyData

The room this process joined, as the lobby callback receives it.

Fields:
  • sessionId : int64 - the matching session, 0 in localMode

  • roomId : int64 - the room within the session

  • hostUserId : int64 - the user hosting the room

  • hostUserName : string - that user’s name

  • inviteData : string - what the invite carried, empty in localMode

  • fromMatching : bool - false for a lobby opened in localMode

  • shouldHost : bool - this process starts the server for the room

Structure macros

ConnectionId

Structure annotation ConnectionId

Session

get_ping_msec(connection: ConnectionId): int

Ping to show players: the smoothed round-trip time, in ms. Same connection rules and 0 as get_round_trip_msec, which is the one to use for delay compensation.

Arguments:
get_round_trip_msec(connection: ConnectionId): int

Round-trip delay to plan for, in ms: a pessimistic estimate, above the usual ping. Server: to the given client; client: to the server, the connection is ignored. 0 until measured.

Arguments:
has_network(): bool

True while this process hosts, is connected or is connecting.

is_server(): bool

Whether this peer decides the game state: the server, and also a game with no network. A real server is has_network() && is_server().

Net commands

broadcast_net_command(data: auto(T)): int

Sends a [net_command] struct: from the server to every client, from a client to the server. Reliability and channel come from the annotation.

Note

A message over the soft limit is sent with a warning in the log. One over the hard limit is dropped by the receiver, and the server also disconnects its sender.

Arguments:
  • data : auto(T)

Returns:
  • int - the number of connections sent to, 0 with no live network.

broadcast_net_command(data: auto(T); to_connection: ConnectionId): int

Server: sends a [net_command] struct to one client. On a client the message goes to the server and the connection is ignored. :Arguments: * data : auto(T)

Returns:
  • int - 1 when sent, 0 for an invalid connection or with no live network.

broadcast_net_command(data: auto(T); to_connections: array<ConnectionId>): int

Server: sends a [net_command] struct to the given clients. On a client the message goes to the server and the list is ignored. :Arguments: * data : auto(T)

Returns:
  • int - the number of connections sent to, 0 with no live network.

Replication rate

get_replication_fps(): float
Returns:
  • float - the rate set on this process. A client does not learn the server’s rate and reads the default.

set_replication_fps(fps: float): float

Server: replication ticks per second, default 20, clamped to [1, 60]; more ticks, more traffic. Never faster than the game tick.

Arguments:
  • fps : float - replication ticks per second.

Returns:
  • float - the value that was applied.

Net transform

is_mirror_interpolated(node: NodeId): bool
Arguments:
Returns:
  • bool - the flag set_mirror_interpolation set, also where a Collider or RigidBody blocks

it; false for a node that does not exist.

is_net_replicated(node: NodeId; component_type: auto(TT)): bool
Arguments:
  • node : NodeId

  • component_type : auto(TT)

Returns:
  • bool - on the authority what set_net_replication left for the type; false for a

type the engine does not replicate, and for a node that does not exist. A client answers false for every type.

is_transform_replicated(node: NodeId): bool

Whether the transform travels on the wire: on the authority its own setting, on a mirror what the authority’s records carry. False for a node that does not exist.

Arguments:
set_mirror_interpolation(node: NodeId; enabled: bool)

Client: shows the mirror between received states, up to 250 ms late, instead of snapping to the newest; off by default. Not applied to a mirror with a Collider or RigidBody. Errors on a node that is not a mirror.

Arguments:
  • node : NodeId

  • enabled : bool

set_net_replication(node: NodeId; component_type: auto(TT); enabled: bool)

Server: whether an engine component type follows the authority on this node’s mirrors, false for every type by default. On, the component arrives on the mirror and follows every change; off, each client keeps its own and the authority’s stays local. A type the engine does not replicate logs an error and changes nothing.

Usage example:

set_net_replication(node, type<Mesh>, true)
Arguments:
  • node : NodeId

  • component_type : auto(TT)

  • enabled : bool

set_transform_replication(node: NodeId; enabled: bool)

Server: whether mirrors follow the node’s local transform and isActive; off by default. Off, a mirror takes them at creation and is placed again when its parent changes, isActive kept; otherwise the client moves it freely. Turning it off stops the mirror where its last record put it.

Arguments:
  • node : NodeId - a node on the authority.

  • enabled : bool - true to replicate transform changes, false to leave them to the client.

teleport_node(node: NodeId)

Server: marks the node’s next transform as a jump, so each mirror is placed on it at once instead of moving to it; set the transform before or after, in the same tick.

Arguments:

Authority

has_node_remote_authority(node: NodeId): bool

True for a mirror on a client: remove_node refuses it. Components and children added on it locally stay removable.

Arguments:
has_remote_authority(comp: Component const?): bool

Whether another peer decides this component’s state. Its replicated fields arrive from that peer and are overwritten on the next frame, so branch on this in on_update to keep authority logic off a replica. False for null.

Arguments:

Ownership

get_node_owner(node: NodeId): ConnectionId
Arguments:
Returns:
  • ConnectionId - on the authority the connection set_node_owner named; on a mirror

LOCAL_PLAYER when the node is this client’s. INVALID_CONNECTION_ID for a node nobody owns and for one that does not exist.

is_node_owner(node: NodeId): bool

Whether this peer drives the node, which is what to ask before reading input for it. False for a node nobody owns, and on the server for nodes clients own.

Arguments:
set_node_owner(node: NodeId; conn_id: ConnectionId)

Server: the connection whose input drives this node; LOCAL_PLAYER for the host’s own node, INVALID_CONNECTION_ID to clear. A client only learns whether the node is its own. Not cleared on disconnect: clear it, or a later connection with the same id inherits the node. Errors on a mirror.

Arguments:

Lobby

leave_lobby_room()

Asks to leave the room. The teardown arrives later through the left and destroyed callbacks.

open_lobby_menu()

Opens the Lobby tab. Does nothing when launched with joinAsClient or autoHost.

set_lobby_callbacks(created: function<(data:LobbyData):void>; established: function<(userName:string):void>; lost: function<(cause:DisconnectCause):void>; left: function<(kicked:bool):void>; destroyed: function<():void>)

Installs all five lobby callbacks at once; null disables one. created is required: without it a new room raises an error and the session is abandoned.

Arguments:
  • created : function<(data: LobbyData):void> - this process joined a room; shouldHost says it is the host

  • established : function<(userName:string):void> - the client reached the host and the Lobby tab closed

  • lost : function<(cause: DisconnectCause):void> - an established connection broke or matching stopped it; the room is left next

  • left : function<(kicked:bool):void> - this process left the room; kicked when it was thrown out

  • destroyed : function<void> - the room closed while a session ran in it

set_lobby_max_connections(max_connections: int = 0)

How many players the next session hosted from this game accepts; 0 means NET_MAX_PLAYERS, and a value above NET_MAX_PLAYERS is clamped to it.

Arguments:
  • max_connections : int