Multiplayer¶
Kanama multiplayer code uses Godot's normal networking stack. Create or join a
peer with wrappers such as ENetMultiplayerPeer, assign it to the scene tree,
and expose synchronized methods and properties from Kotlin scripts.
Peer Setup¶
The common ENet shape mirrors GDScript:
val peer = ENetMultiplayerPeer.create()
peer.createServer(port)
sceneTree.multiplayerPeer = peer
For a client:
val peer = ENetMultiplayerPeer.create()
peer.createClient(address, port)
sceneTree.multiplayerPeer = peer
Use OfflineMultiplayerPeer.create() or clear the tree's peer when returning
to local-only play. Keep the peer owner clear in your code; it is easy to leave
old connection state behind when switching between menu and gameplay scenes.
RPC Methods¶
Declare network-callable methods with @Rpc and register them as Godot
methods:
@RegisterFunction
@Rpc(callLocal = true)
fun jump() {
queuedJump = true
}
KSP generates typed sender helpers next to the script registrar:
PlayerInputSynchronizerRpcs.callLocalJump(input)
PlayerInputSynchronizerRpcs.rpcJump(input)
PlayerInputSynchronizerRpcs.rpcIdJump(input, peerId)
The raw self.rpc("jump") style still exists for dynamic boundaries, but use
generated *Rpcs helpers for Kanama-owned methods. They keep the Godot method
name and parameter list tied to the annotated Kotlin declaration, similar to
generated signal helpers.
callLocalX(...) is generated only for @Rpc(callLocal = true) methods. That
keeps local fallback explicit and prevents accidentally running non-local RPCs
while offline.
Authority And Synchronizers¶
For player input, a useful pattern is:
- The server owns world simulation and spawning.
- Each player scene contains a
MultiplayerSynchronizerfor client-owned input. - The synchronizer's multiplayer authority is assigned before gameplay starts.
- The server reads replicated input and writes authoritative movement state.
Set stable node names for spawned players, usually the peer id as a string, so Godot's multiplayer replication can match scene paths across peers.
When porting from GDScript, preserve the original authority timing. Moving an
authority assignment from _enter_tree to _ready, or from the scene root to a
child synchronizer, can produce code that compiles but silently drops input.
Replicated Properties¶
If a scene's SceneReplicationConfig synchronizes .:motion, the attached
Kotlin script must expose that exact property name:
@ScriptProperty
var motion: Vector2 = Vector2.ZERO
Use @ScriptProperty(name = "...") when the Kotlin property name differs from
the serialized scene name. Built-in node properties such as position,
rotation, and transform are handled by Godot; custom script properties must
be exposed by Kanama.
Run the replication audit on ports that use MultiplayerSynchronizer:
python3 scripts/audit_replicated_script_properties.py /path/to/godot_project
Pass multiple project roots to audit a demo collection in one run.
Runtime Guardrails¶
Avoid raw RPC strings and required node lookups inside hot callbacks such as
@OnPhysicsProcess, @OnProcess, @Rpc, signal lambdas, and coroutine bodies.
Cache required scene nodes during setup, use generated signal/RPC helpers, and
keep generic string calls at real dynamic boundaries.
This audit catches common risky shapes:
python3 scripts/audit_runtime_node_lookups.py /path/to/godot_project/kotlin-src
Pass multiple source roots when reviewing several ports together.
It is intentionally conservative. Treat findings as review prompts: raw RPC
calls should usually become generated *Rpcs calls, and generic dynamic calls
inside runtime callbacks deserve a reason.
Multiplayer Review Checklist¶
Before treating a multiplayer port as stable, review the same boundaries that usually drift during GDScript-to-Kotlin ports:
- authority is assigned at the same lifecycle point as the original project,
especially for nested
MultiplayerSynchronizernodes; - spawned player node names are stable across peers, usually matching peer ids;
- every custom
.:propertyin aSceneReplicationConfigis exposed with@ScriptPropertyor@ScriptProperty(name = "..."); - known Kotlin script targets use generated
*Rpcshelpers or direct typed calls instead of raw string dispatch; and - remaining dynamic
call,rpc, or animation-tree property strings are real dynamic boundaries and are covered by smoke or manual host/client testing.