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 afterset_transform_replication(node, true), an engine component only afterset_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_owneron the server labels it,is_node_owneron 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:
LodSelector - its
lodDistanceandisStatic.Mesh - the asset and the per-instance
coloranduvRect.RigidBody -
velocity,angularVelocity,isSleeping,centerOfMass,inertiaandinertiaRotationstay local (not replicated).
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:
DefaultControlsEvents
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:
Defaultchannel, when sending messages whose relative order is important. This channel already carries engine’s internalRELIABLE_ORDERED, so postingUNRELIABLE_SEQUENCEDmessages on this channel could stagger messages delivery.Controlschannel, when sending messages every frame asUNRELIABLE_SEQUENCED(e.g. input), with no reliable messages on it. Otherwise each lost reliable packet on that channel delays them.Eventschannel, when sending other messages, which must not wait for a lost packet onDefault.
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_updateruns. 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:
connection : ConnectionId
- 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:
connection : ConnectionId
- 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)
to_connection : ConnectionId
- 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)
to_connections : array< ConnectionId>
- 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:
node : NodeId
- Returns:
bool - the flag
set_mirror_interpolationset, 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_replicationleft 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:
node : NodeId
- 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:
node : NodeId
Ownership¶
- get_node_owner(node: NodeId): ConnectionId¶
- Arguments:
node : NodeId
- Returns:
ConnectionId - on the authority the connection
set_node_ownernamed; on a mirror
LOCAL_PLAYERwhen the node is this client’s.INVALID_CONNECTION_IDfor 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:
node : NodeId
- 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:
node : NodeId
conn_id : ConnectionId
Lobby¶
- leave_lobby_room()¶
Asks to leave the room. The teardown arrives later through the left and destroyed
callbacks.
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;
shouldHostsays it is the hostestablished : 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;
kickedwhen it was thrown outdestroyed : 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