Skip to content

Networking

Server-authoritative multiplayer: replicated state, remote procedure calls, client prediction, and a headless server build.

Not verified end to end in 0.1.0

Everything on this page exists and compiles, and the headless server has been measured holding 30 Hz with a real scene, real physics and real C#. Two things are worth knowing before you build on it:

  • The transport and the API have not been run together in a real game session. Individual pieces are tested — a real UDP server and client stand up and pass their checks — but "a multiplayer game, played" is not among the things that have happened.
  • Secure web transport is not available. The vendored HTTP library has no TLS backend beyond OpenSSL, which isn't linked. Anything needing wss:// or https:// is blocked.

Treat this as a preview. It's documented because it's a large, real API and finding out it exists by reading source is worse — not because it's ready to ship a game on.

The shape of it

One peer is the server and owns the truth. Clients send inputs and receive state. A host is both at once.

  • State — a value that has a current truth (position, health, score) — is replicated.
  • Events — a thing that happened (a gunshot, a chat message) — are RPCs.

Getting that split right is most of multiplayer. A position sent as an RPC is a stutter; a gunshot sent as state is a sound that plays late or not at all.

Net

The session.

MemberWhat it is
Net.IsServerThis peer owns the truth. True on a host
Net.IsClientThis peer has a local player. True on a host
Net.IsHostBoth at once
Net.IsRunningA session is active
Net.LocalClientIdThis peer's client id
Net.TickThe current network tick
Net.ConnectionCountHow many peers are connected
Net.GetConnectionId(int index)The client id at that index
Net.GetRtt(ulong clientId)Round-trip time to that client, in milliseconds

Net.Spawn(string name, ulong owner = 0)

Spawns a networked entity from a template, on every peer. Server only. owner of 0 means the server owns it.

csharp
GameEntity player = Net.Spawn("Player", owner: clientId);

Net.Despawn(GameEntity entity)

Removes it everywhere. Server only.

Net.Kick(ulong clientId)

Disconnects a client. Server only.

NetEntity

Derive from NetEntity instead of GameEntity for anything that exists on the network. It is a GameEntity, so everything on that page still applies.

Role

A NetRole flag set describing this peer's relationship to this entity, recomputed on read — so an authority change mid-session is visible immediately rather than at the next spawn.

FlagMeaning
NetRole.ServerThis peer owns the truth
NetRole.ClientThis peer has a local player
NetRole.OwnerThis peer owns this entity
NetRole.ObserverThis peer can see it but doesn't own it

They compose: on a host, a locally-owned player's role is Server | Client | Owner.

IsOwner / IsServer

Shorthands for the two checks you write most.

Lifecycle hooks

HookWhen
OnNetSpawn(NetRole role)Once this entity exists on the network, on every peer that can see it
OnNetDespawn(NetRole role)When it goes away
OnAuthorityChanged(bool gained)Ownership moved to or from this peer
OnPredictedTick(int tick)A predicted simulation step on the owning client
OnReconcile(int tick)The server's truth disagreed with a prediction
csharp
public class Player : NetEntity
{
    protected override void OnNetSpawn(NetRole role)
    {
        if (role.HasFlag(NetRole.Owner))  AttachCamera();    // my player only
        if (role.HasFlag(NetRole.Server)) ResetStats();
        ShowNameplate();                                     // every observer
    }
}

One question asked once

Other engines give you eight separate callbacks for this — OnStartLocalPlayer, OnStartServer, OnStartClient and so on. Running camera setup on every observer instead of only the owner is the classic bug they exist to prevent, but it's one question asked four ways. A flag set answers it once, composes on a host, and gains a new role without gaining a new callback.

NetIdentity

Every GameEntity has a NetIdentity — always safe to read, whether or not the game is networked.

MemberWhat it is
NetIdNetwork id. 0 if not spawned on the network
OwnerClientIdWhich client owns it
IsOwnerWhether this peer owns it
IsSpawnedWhether it's on the network at all
GrantAuthority(ulong clientId)Hand ownership to a client. Server only
RevokeAuthority()Take it back. Server only

IsSpawned is false for every entity in a single-player game, so guarding on it is how shared code stays single-player-safe.

[Rpc]

Marks a method as a remote procedure call. The body you write is what runs on the receiving end — calling the method sends it.

csharp
[Rpc(To.Server)]
private void Fire(Vector2 direction)
{
    // Runs on the server, having been called on the owning client.
    SpawnProjectile(direction);
}

[Rpc(To.Observers, ExcludeOwner = true)]
private void PlayRemoteSound() => m_Shot?.Play();

[Rpc(To.Server, RequireOwner = false)]
private void RequestJoin(int team) { }

To

ValueDirection
To.ServerOwner → server. The client asks; the server decides
To.ObserversServer → everyone who can see the entity
To.OwnerServer → the one client that owns the entity
To.ClientServer → one named client

Options

OptionDefaultWhat it does
RequireOwnertrueOnly the owning client may call it
ExcludeOwnerfalseSkip the owner when broadcasting to observers — they already played the effect locally
ChannelChannel.ReliableDelivery guarantee

Channel

ValueGuarantee
ReliableGuaranteed and ordered. An event that is lost has simply not happened
UnreliableSequencedMay be dropped; late arrivals discarded. For a stream where only the newest matters
UnreliableMay be dropped, any order

RPCs are events, not state

A position belongs in a replicated field. A gunshot belongs in an RPC. If you find yourself sending the same RPC every tick, you wanted replication.

The rewrite is done by a source generator, so the generated code is real, steppable C# — not an IL weaver you can't step through.

[Replicated]

Marks a field as server-owned state, sent to clients automatically.

OptionWhat it does
IntervalMinimum seconds between updates. 0 means every tick
OnChangeName of a method to call on clients when the value changes
csharp
[Replicated(OnChange = nameof(OnHealthChanged))]
private int m_Health = 100;

[Replicated(Interval = 0.5f)]
private int m_Score;

private void OnHealthChanged(int oldValue, int newValue)
{
    m_HealthBar.Value = newValue / 100f;
}

[Predicted]

Marks a field as part of the client's predicted state, so the owning client can simulate ahead of the server and reconcile when the server disagrees. Pair it with OnPredictedTick and OnReconcile.

Guard attributes

AttributeEffect
[ServerOnly]The method throws if called anywhere but the server
[ClientOnly]Likewise, for clients
[ServerOnlyTick]The method only runs on server ticks
[ClientOnlyTick]The method only runs on client ticks

These turn "I forgot this only runs on the server" from a subtle desync into an immediate, obvious failure.

Lower-level pieces

NetRuntime, NetInternal, NetPayloadWriter and NetPayloadReader are what the source generator emits calls into — RPC name hashing, dispatch, and payload serialization. Game code shouldn't need them; they're public because generated code has to reach them.

The headless server

A JOYSTICK_HEADLESS build produces a server binary with no window, no graphics backend, and no GLFW or SDL linked at all — it loads a scene, steps physics and runs your C# on a fixed tick. It's configured through server.yaml, environment variables or the command line, and shuts down cleanly on SIGTERM.

This is the part that has actually been measured: a headless binary holding 30 Hz with zero overruns.

See also

  • GameEntity — everything NetEntity inherits
  • Attributes — the non-networking attributes
  • Time — the fixed clock a tick loop runs on