Skip to content

06 — RPCs

A remote procedure call sends a one-shot message with a payload to other peers, routed by a target and delivered reliably or unreliably. Unlike replicated state (which converges to a value), an RPC is an event you handle once.

Verified against: bindings/lattice-unity/com.lattice.netcode/Runtime/NetworkRunner.cs:310-315, NetworkObject.cs:126-135, LatticeNative.cs:65-71,292-295.

Sending an RPC

There are two entry points — a runner-level call that takes an explicit netid, and an object-scoped convenience on NetworkObject:

NetworkRunner.Rpc (object referenced by netid)
public void Rpc(ulong netid, ushort rpcId, LatticeRpcTarget target, byte[] payload, bool reliable = true);
NetworkObject.Rpc (scoped to this object)
public void Rpc(ushort rpcId, LatticeRpcTarget target, byte[] payload, bool reliable = true);

Verified against: NetworkRunner.cs:310-315 and NetworkObject.cs:127-135.

  • rpcId — a ushort you choose to identify the call (like an opcode).
  • payload — a byte[] you encode yourself (a null payload is treated as empty).
  • target — who receives it (below).
  • reliabletrue (default) guarantees delivery + ordering; false is fire-and-forget.

Example:

Fire an RPC
// object-scoped, to everyone, reliably
board.Rpc(rpcId: 7, LatticeRpcTarget.All, payload: BitConverter.GetBytes(score));

// runner-level, to the server only, unreliably
runner.Rpc(netid: board.NetId, rpcId: 3, LatticeRpcTarget.Server, payload, reliable: false);

Targets

LatticeRpcTarget selects the routing:

Target Value Delivers to
Server 0 The authority (a client's RPC implicitly goes here)
Owner 1 The object's owner (input authority)
All 2 Every peer
AllButOwner 3 Everyone except the owner

Verified against: LatticeNative.cs:65-71.

Receiving an RPC

Subscribe to OnRpc. It gives you the netid the RPC was scoped to, the rpcId, and a managed copy of the payload (valid after the callback returns):

Handle incoming RPCs
runner.OnRpc += (netid, rpcId, payload) =>
{
    switch (rpcId)
    {
        case 7: int score = BitConverter.ToInt32(payload); /* … */ break;
        case 3: /* … */ break;
    }
};

Verified against: NetworkRunner.cs:29-30 (event), NetworkRunner.cs:156-157 (the trampoline copies the payload into a fresh byte[] via CopyPayload).

The payload is copied for you

The native payload pointer is valid only during the call; the binding copies it into a managed byte[] before invoking your handler, so you can keep it. An empty/null payload arrives as Array.Empty<byte>().
Verified against: NetworkRunner.cs:190-203 (MaxPayloadBytes + CopyPayload).

Reliability

The final reliable flag maps straight to the core's reliable-vs-unreliable channels:

  • reliable: true (default) — guaranteed delivery, in order. Use for gameplay-critical, infrequent messages (a move, a purchase, "player died").
  • reliable: false — may be dropped or reordered; cheaper. Use for high-frequency, loss-tolerant hints where the latest wins.

Verified against: the reliable ? 1 : 0 argument threaded into lattice_rpc (NetworkRunner.cs:314, NetworkObject.cs:132).

Is the RPC path real? Yes — and it's verified

The RPC path is exercised end to end by the binding's own headless P/Invoke test against the real native library. It performs a client → server RPC (RpcTarget.Server, "ping") and a server → ALL RPC ("pong") and asserts both arrive with the correct id and payload.

Verified against: bindings/lattice-unity/README.md (ServerClient_Replicate_And_Rpc_EndToEnd, step "client→server RPC … and server→ALL RPC … assert both arrive with the right id and payload"), driven from bindings/lattice-unity/tests/Lattice.Headless.Tests/EndToEndPInvokeTests.cs.

Why the tic-tac-toe sample uses events instead of RPCs

Here is an important, honest caveat. The tic-tac-toe sample does not call Rpc at all — it sends moves and chat through the custom-event API (SendEvent / OnEvent, chapter 07). The reason is specific:

on_event surfaces the originating sender peer id; on_rpc does not. The host needs the sender to map a peer to its assigned mark and validate that a move comes from the side whose turn it is.

So for a host-authoritative design where the server must know who sent a message, events are the right tool; RPCs are ideal when the sender's identity doesn't matter to the handler (broadcasts, notifications, owner-directed effects).

Verified against: TicTacToeSession.cs:1-8 (design comment), TicTacToeProtocol.cs:66-69, samples/unity-tictactoe/README.md ("Moves ride the event API rather than an object RPC specifically because on_event exposes the originating sender peer id, which the host needs to validate … on_rpc does not surface the sender").

Choosing between an RPC and an event

Use an RPC when… Use an event when…
The handler doesn't need the sender's identity The receiver must know who sent it (server-side validation)
It's naturally scoped to an object (netid) It's session-global (no object) — use SendEvent
You want Fusion-style [Rpc] ergonomics You want a directed send to one peer (SendEventToPeer)

Both ride the same reliable/unreliable channels and the same target routing.


Next: 07 — Events & presence, the message channel the sample actually uses.