06 — RPCs¶
A remote procedure call invokes a named method on other peers with a typed argument list, routed by a target and delivered reliably or unreliably. Unlike replicated state (which converges to a value), an RPC is a one-shot call you handle once.
In the Godot binding an RPC is scoped to a spawned object and sent through its LatticeSync. The
tic-tac-toe sample uses one — the "place" move RPC.
Verified against: bindings/lattice-godot/src/lattice_sync.cpp:300-369,
samples/godot-tictactoe/godot/scripts/board.gd.
Sending an RPC¶
method— the name of the method to call on the receiving peers' parent node (a string, like"place"or"fire").args— a typedArray; each element is packed by its Variant type (see the table below).target— one of theLatticeSyncauthority ordinals (below). Required — onlyreliablehas a default.reliable—true(default) guarantees delivery + ordering;falseis fire-and-forget.
Verified against: lattice_sync.cpp:63-64 (bound with a single DEFVAL(true) for reliable, so
target has no default), lattice_sync.h:87-91.
The sample's move request, from board.gd:
func request_move(cell: int, mark: int) -> void:
if sync.has_authority():
_apply_move(cell, mark) # host applies directly
else:
sync.send_rpc("place", [cell], LatticeSync.STATE_AUTHORITY) # client asks the host
Verified against: board.gd:74-78.
The method string is the actual method name — the id is derived for you
On the wire an RPC carries a small numeric rpc_id, not the string. The binding assigns that id the
first time it sees a method name, appending to a per-object list — so peers that share the same
build assign the same ids in the same order. You just pass the method name.
Verified against: lattice_sync.cpp:303-306 ("rpc_id = index of this method name … The order
is established the first time a name is seen, identical on peers that share the build").
Targets¶
The target is a LatticeSync authority ordinal; the binding maps it to the core's RPC routing:
| Ordinal | Value | Maps to | Delivers to |
|---|---|---|---|
LatticeSync.STATE_AUTHORITY |
0 | LATTICE_RPC_SERVER |
The authority (a client's RPC implicitly goes here) |
LatticeSync.INPUT_AUTHORITY |
1 | LATTICE_RPC_OWNER |
The object's owner (input authority) |
LatticeSync.ALL_PEERS |
2 | LATTICE_RPC_ALL |
Every peer |
LatticeSync.OTHER_PEERS |
3 | LATTICE_RPC_ALL_BUT_OWNER |
Everyone except the owner |
Verified against: lattice_sync.h:47-53 (the Authority enum), lattice_sync.cpp:307-309
(send_rpc maps the ordinal via lgodot::glue_rpc_target); thirdparty/lattice/lattice.h:109-113
(the lattice_rpc_target values).
Argument types¶
Each element of args is packed by its Godot Variant type:
| Godot type | Packed as |
|---|---|
bool |
BOOL |
int |
LONG (64-bit) |
float |
DOUBLE |
Vector2 / Vector3 / Vector4 |
VEC2 / VEC3 / VEC4 |
Quaternion |
QUAT |
String |
STRING (length-prefixed UTF-8) |
PackedByteArray |
BYTES |
Verified against: lattice_sync.cpp:31-57 (pack_variant_args, the Variant→RpcArg mapping shared
by send_rpc and send_event).
Receiving an RPC¶
You do not subscribe to a signal for RPCs. Instead, the binding calls the matching method on the
spawned object's parent node — the same node the LatticeSync is a child of. So you just define a
method with the name you sent. The sample defines place:
# RPC handler invoked on the AUTHORITY when a client sends "place".
func place(cell: int) -> void:
if not sync.has_authority():
return
_apply_move(cell, TttRules.MARK_O)
Verified against: board.gd:82-85; lattice_sync.cpp:339-369 (_dispatch_rpc rebuilds a Godot
Array from the payload and calls p->callv(StringName(rpc_methods_[rpc_id]), gd) on the parent).
The received arguments are decoded back to Godot Variants (int→int, Vector3→Vector3, etc.) and
passed positionally to your method.
Verified against: lattice_sync.cpp:345-366 (the per-kind decode into the call array).
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).reliable: false— may be dropped or reordered; cheaper. Use for high-frequency, loss-tolerant hints where the latest wins.
Verified against: lattice_sync.cpp:315-317 (reliable ? 1 : 0 threaded into lattice_rpc).
RPC vs. event: what the sample chose, and why¶
An important, honest point specific to Godot: the tic-tac-toe sample uses an RPC for moves (the
"place" call above) but uses the custom-event API for chat (chapter 07).
The reason is the same trade-off you'd reason about in any engine:
An RPC does not surface the sending peer's id; a custom event does (
on_eventcarriessender). The move RPC works fine because this is a fixed two-player game — the only client is always O, so the host'splacehandler can hardcodeMARK_Owithout needing to know who sent it. Chat needs the sender (to label and relay each line authoritatively), so it rides the event path.
The board.gd comment says exactly this: "In this 2-player sample the only client plays O. (For >2
players, map sync.get_input_authority() → mark.)"
Verified against: board.gd:80-85 (the place handler hardcodes MARK_O with the ">2 players"
note); lattice_runner.cpp:555-567 (_on_rpc carries netid/rpc_id/payload but no sender),
versus lattice_runner.cpp:569-579 (_on_event carries 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 a method call on a spawned object | It's session-global, or you want a directed send to one peer |
Both ride the same reliable/unreliable channels and the same target routing.
Is the RPC path real? Yes — and it's verified¶
The RPC path is exercised end to end by the sample's headless harness against the real native library:
the client sends "place" to the authority, the host validates and applies it, and the change
replicates back. The harness reports 60 checks, 0 failures.
Verified against: samples/godot-tictactoe/README.md ("a legal move replicates host→client";
"60 checks, 0 failures"), samples/godot-tictactoe/harness/ttt_harness.cpp.
Next: 07 — Events & presence, the object-less message channel that carries the sender's identity.