Skip to content

Writing Kotlin Scripts

Kanama scripts are Kotlin classes that Godot loads as .kt script resources. This page highlights the Kotlin script model and the most common differences from GDScript.

Naming Conventions

GDScript uses snake_case everywhere. Kanama follows Kotlin conventions.

GDScript Kotlin
func move_and_slide() fun moveAndSlide()
var my_property = 0 var myProperty: Long = 0
const MAX_SPEED = 10 const val MAX_SPEED = 10L
class MyNode class MyNode (same)

Godot's API wrappers convert names automatically — move_and_slide() becomes moveAndSlide(), get_position() becomes getPosition(). Signal name strings keep their original snake_case form in the .Signals companion objects.

this Is Not Your Node

In GDScript self is the node. In Kanama this (the implicit Kotlin receiver) is the JVM script object — not the Godot node the script is attached to.

Extend KanamaScript<T> to get a typed self wrapper for the attached node:

@ScriptClass(attachTo = "CharacterBody3D")
class Player(godotObject: MemorySegment) :
    KanamaScript<CharacterBody3D>(godotObject, ::CharacterBody3D) {

    @OnPhysicsProcess
    fun physicsProcess(delta: Double) {
        if (self.isOnFloor()) self.moveAndSlide()
    }
}

Inside a KanamaScript<T>, use selfAs for a secondary view of the same Godot object:

private val node3d = selfAs(::Node3D)

See Kotlin Style — self And selfAs Caching.

Scalar Types

GDScript Kanama Note
int Long Use Long at all GDExtension boundaries
float (method args/returns) Double Godot's ABI uses 64-bit slots for scalar float
bool Boolean
Vector3.x/y/z Float (real_t) Matches Godot's default single-precision storage
delta in _process Double Engine timing is always double-precision

The float/double split matches the official C# binding. Vector components are Float so you can pass them without .toDouble() noise; delta and scalar method arguments are Double for the same reason. See Kotlin Style — Float / Double.

Value Type Mutation

GDScript structs modify in-place:

velocity.y += gravity * delta

Kanama value types (Vector2, Vector3, etc.) are immutable. Use the with* helpers or construct a new value:

body.velocity = body.velocity.withY(body.velocity.y + gravity * delta)

Lifecycle Callbacks

GDScript uses overridable virtual methods. Kanama uses annotations:

GDScript Kanama annotation
func _ready(): @OnReady
func _process(delta): @OnProcess
func _physics_process(delta): @OnPhysicsProcess
func _enter_tree(): @OnEnterTree
func _exit_tree(): @OnExitTree

The function name is up to you — the annotation is what wires it up. Drop the leading underscore; fun ready() is the convention.

Exports

GDScript Kanama
@export var speed = 5.0 @ScriptProperty var speed: Double = 5.0
@export_group("Movement") @ExportGroup("Movement") on first property in group
@export var scene: PackedScene @ScriptProperty var scene: PackedScene? = null
@tool @Tool on the class

Use @ScriptProperty for @ScriptClass scripts and @RegisterProperty for @RegisterClass types. @Export works as an alias in both contexts. See Exports and Resources.

Printing

GD.print("Hello from Kotlin")       // mirrors C# GD.Print
System.err.println("[debug] $value") // also appears in Godot output

Rebuild Required

GDScript changes are live. Kotlin scripts must be compiled before Godot sees them. Use the Build Scripts toolbar button (Kanama Tools plugin) or:

./gradlew installAddonJar -PkanamaProjectDir=... -PkanamaProjectScriptsDir=...

Hot reload reloads the jar without restarting Godot. See The Editor Loop.

Packages Are Mandatory

Every script file must declare a package. Scripts without a package cannot reference other scripts and cause issues with generated registrars:

package com.mygame.scripts

@ScriptClass(attachTo = "Node")
class MyScript(godotObject: MemorySegment) :
    KanamaScript<Node>(godotObject, ::Node) { ... }

Signals

See Signals and Callbacks for the full reference. Quick comparison:

# GDScript
signal hit_enemy(damage)
emit_signal("hit_enemy", 10)
// Kanama
@Signal
fun hitEnemy(damage: Long) = Unit

PlayerSignals.hitEnemy(this, 10L)

For multiplayer RPC methods, KSP also generates typed *Rpcs helpers from @Rpc declarations. See Multiplayer.

Registered Method Helpers

KSP generates a *Methods helper object for every Kanama script method registered with @RegisterFunction. Use these helpers when Kotlin code needs to invoke another Kanama script's public Godot-facing method:

@RegisterFunction
fun damage(amount: Double) {
    health -= amount
}

// Direct script instance.
PlayerMethods.damage(playerScript, 5.0)

// Generic GodotObject from a collision or lookup.
if (!PlayerMethods.damage(collider, 5.0)) {
    // The collider was not backed by Player.
}

For non-Unit methods, the GodotObject overload returns a nullable value:

val label = HudMethods.currentLabel(hudObject) ?: return

These helpers are for Kanama-to-Kanama calls. For mixed GDScript/Kanama projects, keep using signals, registered Godot names, or GodotObject.call where the target is a GDScript object or an intentionally dynamic autoload.

Node Lookup

GDScript's $NodeName shorthand does not exist in Kanama. Use exported @ScriptProperty references (preferred) or typed lookup helpers:

// Preferred: let the inspector wire it
@ScriptProperty var label: Label? = null

// Manual lookup for required scene structure
val label = self.requireAs("Label", ::Label)

Nested paths use the same slash-separated NodePath strings that Godot scene files and GDScript use:

@onready var animation_player = $Character/AnimationPlayer
private val animationPlayer by lazy {
    self.requireAs("Character/AnimationPlayer", ::AnimationPlayer)
}

Use requireAs for nodes that must exist for the scene to work. Use getAsOrNull when the node is optional:

val optionalMarker = self.getAsOrNull("Markers/Spawn", ::Node3D)

Cross-Script References

To reference another Kanama script type (e.g. var target: Vehicle?), both classes must be in the same project and the referenced class must be @GlobalClass. See Calling Godot APIs — Global Classes.

Known Gotchas

  • Registered methods have no default arguments from Godot's side. If a @RegisterFunction method has Kotlin default parameters, Godot still requires all arguments when calling it. Pass them explicitly or use overloads.
  • KSP must run before IntelliJ resolves generated helpers like PlayerMethods, PlayerSignals, or PlayerRpcs. Run a Gradle sync or build after adding new @RegisterFunction, @Signal, or @Rpc declarations.
  • @Tool scripts run in the editor. Guard editor-only code against partially initialized scenes — exported node references may be null during editor tool execution. From a KanamaScript subclass, use isEditorHint() for editor checks and notifyInspectorChanged() after changing editor-time property-list state.