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:
public void Rpc(ulong netid, ushort rpcId, LatticeRpcTarget target, byte[] payload, bool reliable = true);
public void Rpc(ushort rpcId, LatticeRpcTarget target, byte[] payload, bool reliable = true);
Verified against: NetworkRunner.cs:310-315 and NetworkObject.cs:127-135.
rpcId— aushortyou choose to identify the call (like an opcode).payload— abyte[]you encode yourself (anullpayload is treated as empty).target— who receives it (below).reliable—true(default) guarantees delivery + ordering;falseis fire-and-forget.
Example:
// 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):
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_eventsurfaces the originating sender peer id;on_rpcdoes 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.