Skip to content

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

LatticeSync.send_rpc
sync.send_rpc(method: String, args: Array, target: int, reliable: bool = true)
  • method — the name of the method to call on the receiving peers' parent node (a string, like "place" or "fire").
  • args — a typed Array; each element is packed by its Variant type (see the table below).
  • target — one of the LatticeSync authority ordinals (below). Required — only reliable has a default.
  • reliabletrue (default) guarantees delivery + ordering; false is 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:

Client asks the authority to place a mark — 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:

board.gd — the RPC handler runs on the authority
# 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, Vector3Vector3, 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_event carries sender). The move RPC works fine because this is a fixed two-player game — the only client is always O, so the host's place handler can hardcode MARK_O without 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.