Signals and Callbacks¶
Kanama supports custom signal declaration and emission for registered classes, plus Callable-based connection helpers for Godot-owned objects and Kanama script instances.
Custom Signals¶
Declare custom signals with @Signal. KSP generates a typed *Signals helper
for each @RegisterClass and @ScriptClass declaration:
@Signal
fun pinged(value: Long) = Unit
@RegisterFunction
fun ping(): Long {
HelloScriptSignals.pinged(this, counter)
return counter
}
These helpers live in net.multigesture.kanama.generated and are produced by
the Kanama KSP processor in the game project. External projects should apply
the KSP plugin and depend on net.multigesture.kanama:processor through KSP;
then IDEs see the generated source directory after Gradle sync.
When porting a GDScript custom signal, declare the signal with a Kotlin camelCase method name. KSP exposes it to Godot as snake_case and generates a typed emitter:
signal coin_collected
coin_collected.emit(coins)
@Signal
fun coinCollected(value: Long) = Unit
PlayerSignals.coinCollected(this, coins)
The Kotlin declaration exists so Godot sees coin_collected in script metadata
and saved .tscn connections can keep referring to that signal.
Generated signal helpers also include typed handles for code connections and
awaits. For a coinCollected(value: Long) signal, KSP emits
PlayerSignals.signalCoinCollected(...),
PlayerSignals.connectCoinCollected(...), and
PlayerSignals.awaitCoinCollected(...) alongside the existing typed emitter.
The string-based signal(PlayerNames.Signals.coinCollected) style remains
available when you need lower-level Godot API behavior.
RPC methods follow the same generated-helper pattern. See
Multiplayer for @Rpc sender helpers such as
PlayerRpcs.rpcJump(...) and PlayerRpcs.callLocalJump(...).
Connecting Signals¶
For existing Godot signals, use signal(name) when you want a small handle:
import net.multigesture.kanama.generated.PlayerNames
@RegisterFunction
fun onBodyEntered(body: GodotObject) {
if (body.isClass("CharacterBody3D")) {
collectCoin()
}
}
// PlayerNames.Methods.onBodyEntered is generated from the
// @RegisterFunction above. It is the Godot-facing name "on_body_entered".
area.signal(Area3D.Signals.bodyEntered)
.connect(self, PlayerNames.Methods.onBodyEntered)
The connection layer mirrors Godot's Callable model. The target is a Godot
object or Kanama script instance, and the method name is the Godot-facing method
name. For @RegisterFunction fun onBodyEntered(...), that default name is
on_body_entered; if you use @RegisterFunction(name = "..."), connect to the
explicit name.
KSP generates *Names sidecar objects for Kanama classes, including Methods,
Properties, and Signals constants. Use those constants when referring to
Kanama-owned names from string-based Godot APIs. For common Godot-owned
signals, wrappers expose small Signals constant objects as they are added,
such as Node.Signals.childEnteredTree, Area3D.Signals.bodyEntered, and
BaseButton.Signals.pressed.
Scene Connections¶
Scene files can store signal connections, but you can also wire them explicitly from code when you want the relationship to be searchable during a port:
val player = node.requireAs("Player", ::Node)
val hud = node.requireAs("HUD", ::Node)
player.signal(PlayerNames.Signals.coinCollected)
.connect(hud, HudNames.Methods.onCoinCollected)
Both forms use the same Godot signal system. Scene connections are visible in
the editor and serialized in .tscn; code connections are often easier to
review because the emitter, receiver, and method name live together.
Saved scene connections are strict because the method name is stored in the
.tscn file. If a scene was created from GDScript and connects to
_on_body_entered, the Kotlin method must expose that exact Godot-facing name:
@RegisterFunction("_on_body_entered")
fun onBodyEntered(body: Node) {
collect()
}
Without the explicit name, Kanama exposes the method as on_body_entered,
which does not match the saved scene connection. The same rule applies to
custom signal receivers such as _on_coin_collected.
To catch this during ports, run:
python3 /path/to/kanama/scripts/scene_connection_lint.py /path/to/godot_project
The lint walks .tscn connections, resolves target nodes with attached Kotlin
scripts, and verifies the target method is exposed by @RegisterFunction or
@Method. KSP also warns when a public onSomething method in a
@ScriptClass is not registered, since that often means a scene signal
callback was missed.
Dynamic Scene Instances¶
For dynamically instantiated scenes, code connections often look a little more explicit because both sides are Godot objects at runtime. In the Godot 3D Squash the Creeps demo, the original GDScript wires each spawned mob to the score label like this:
mob.squashed.connect($UserInterface/ScoreLabel._on_Mob_squashed)
The Kanama equivalent uses explicit objects and Godot-facing names:
val mob = mobScene.instantiate()
val scoreLabel = self.requireAs("UserInterface/ScoreLabel", ::Label)
val mobScript = mob.kotlinScriptInstance<Mob>()
?: error("Instantiated mob scene is not backed by Mob")
mobScript.initialize(mobSpawnLocation.position, playerPosition)
mob.signal(MobNames.Signals.squashed)
.connect(scoreLabel, ScoreLabelNames.Methods.onMobSquashed)
Read this as:
kotlinScriptInstance<Mob>()retrieves the Kotlin script object attached to the spawned scene root, so Kotlin-to-Kotlin calls can usemobScript.initialize(...)instead ofGodotObject.call("initialize", ...).mobis the specific scene instance that will emit.mob.signal(MobNames.Signals.squashed)selects the signal on that instance.connect(...)tells Godot which object and Godot-facing method to call when the signal fires.
Prefer typed Kotlin calls when both scripts are Kanama scripts. Use
GodotObject.call("method_name", ...) when crossing a dynamic boundary:
calling into GDScript, calling editor/runtime APIs whose methods are not
wrapped yet, or invoking methods discovered by name at runtime.
Lambda Callbacks And Await¶
For Kotlin lambda callbacks, generated @ScriptClass instances expose small
dispatcher methods that Godot can call through its Callable system. Pass the
script's owning object as the target so Godot has a real object lifetime to
connect against:
val connection = area.signal(Area3D.Signals.bodyEntered).connectObject(self) { body ->
if (body.isClass("CharacterBody3D")) {
collect()
}
}
connection.close()
connect(target, argumentCount) { args -> ... } supports zero to three emitted
arguments today. connectObject is the common one-argument shortcut for signals
such as body_entered. await(target, argumentCount) and awaitObject(target)
are cancellation-aware suspend helpers built on the same dispatch path.
For custom Kanama signals, prefer the generated typed helper when both the emitter and receiver are Kotlin-visible:
val connection = PlayerSignals.connectCoinCollected(playerScript, self) { coins ->
updateHud(coins)
}
Generated method dispatch accepts common object wrappers directly, so a method
connected to body_entered can take a typed wrapper:
@RegisterFunction
fun onBodyEntered(body: Node3D) {
body.hide()
}