Calling Godot APIs¶
Kanama should feel familiar to Godot users coming from GDScript or C#, while still being honest about the places where Kotlin/JVM and GDExtension have different constraints. This section mirrors the Godot C# API documentation topics and tracks the Kotlin equivalent.
API Differences¶
Kanama exposes two layers:
- Godot-shaped wrappers — stay close to engine names and GDExtension signatures. Useful for version alignment and debugging.
- Kotlin conveniences — lower-camel names, Kotlin properties, extension helpers, and coroutine integration.
Wrapped Classes¶
Kanama now promotes generated wrappers in broad slices rather than maintaining a hand-written class list in this page. Use API Coverage for the current promoted class/method totals and Wrapper Generator Report for the generator reach and skip categories.
When writing gameplay code, prefer typed wrappers such as Node, Node3D,
CharacterBody3D, Area3D, AnimationPlayer, Control, ResourceLoader,
Input, and Mathf whenever they exist. For your own Kanama scripts, prefer
generated *Methods, *Signals, *Rpcs, and *Names helpers. Use
GodotObject.call(...) only at dynamic boundaries such as mixed
GDScript/Kanama interop or APIs that are not wrapped yet.
Generated KDoc¶
Public Godot-backed wrappers and builtin value types carry generated KDoc
imported from Godot's official doc/classes/*.xml files. In IntelliJ, this
means wrapped Godot APIs show useful tooltips and documentation while writing
Kotlin scripts. Game projects do not need to run any documentation-generation
commands.
Value Types¶
Builtin value types such as Vector2, Vector3, Color, and Transform3D
are immutable snapshots in Kanama. This intentionally makes copied Godot values
less error-prone: changing a component means creating a new value and assigning
it back to the Godot property, not mutating a hidden copy.
Value-type helpers mirror Godot behavior where possible, including transform and physics math. Gameplay code can treat these helpers as normal Kotlin value APIs and assign the updated value back to the Godot property.
Node lookup helpers: getNodeOrNull, getAsOrNull(path, ::Class),
getNodeAsOrNull(path, "ClassName", ::Class), requireAs(path, ::Class).
String and NodePath overloads available for all lookup helpers.
Deferred mutation: GodotObject.setDeferred(property, value).
Interop with GDScript autoloads: GodotObject.call(method, vararg args) —
see the GDScript interop section below.
GodotObject.call(method, vararg args) is available as a mixed-project
interop path for scalar Variant arguments/returns, Vector2/Vector3,
Color, GodotObject, and Resource arguments. Vector and color returns
decode to Kotlin value types; object returns decode as non-owning
GodotObject wrappers. This is intended for cases such as calling a GDScript
autoload while porting a project incrementally:
val audio = self.getNodeOrNull("/root/Audio")
audio?.call("play", "res://sounds/jump.ogg")
For built-in Godot names used at dynamic boundaries, Kanama generates
engine-wide constants from extension_api.json:
audio?.call(MethodName.play, "res://sounds/jump.ogg")
player.signal(SignalName.treeExited)
self.getTree().setGroup("enemies", PropertyName.visible, false)
Script-local generated names such as PlayerNames.Methods.onBodyEntered remain
the right choice when Godot APIs need a method, property, or signal name. When
Kotlin code is invoking another Kanama script method directly, prefer the
generated PlayerMethods.damage(...)-style helpers instead of string dispatch.
For now, project autoloads should be resolved through the scene tree root
(/root/<Name>). Engine.getSingleton() remains useful for engine singletons,
but project autoloads are not treated as engine singletons in the current smoke
path.
Collections¶
Godot collections only matter when data crosses the engine boundary. For pure
Kotlin game logic, prefer Kotlin/JDK collections (List, MutableList, Map,
and arrays) because they avoid per-element engine marshalling.
Current Kanama collection support is intentionally narrow:
PackedStringArrayis exposed asList<String>in wrappers such asDirAccess.getFilesAt,ResourceLoader.listDirectory, andProjectSettings.getChangedSettings. Scene-authoredPackedStringArrayandArray[String]values also decode toList<String>when they pass through generic Variant paths such as@ScriptPropertysetters.PackedByteArrayis exposed asByteArrayinFileAccessbyte helpers.- Selected object arrays are exposed as non-owning Kotlin wrapper lists, such
as
Area3D.getOverlappingBodies()andArea3D.getOverlappingAreas(). - Scalar
Dictionaryvalues are exposed asMap<String, Any?>where the wrapper knows the dictionary shape, such asProjectSettingsand selected singleton metadata calls. Exported@ScriptPropertymaps additionally register as typedDictionaryslots — see Exporting Dictionaries. Vector2andVector3include Kotlin-side arithmetic and common gameplay math helpers such aslength,normalized,dot,lerp, distance, and length limiting;Vector3also includescrossandrotated.BasisandTransform3Dexpose Godot-style 3D transform math:basis * vector,transform * vector,basis.determinant(), andbasis.inverse().Basis.x,Basis.y, andBasis.zmatch Godot's public column-axis API, even though the GDExtension native memory block is packed as rows internally.
General public Array/Dictionary wrappers and broad Kotlin collection
conversion are intentionally conservative. For APIs outside the promoted
shapes above, use typed wrappers where available or call through explicit
dynamic Godot APIs at the boundary.
Variant¶
Kanama already marshals the common scalar Variant-compatible types used by the
current API surface: null, Boolean, Long/Int, Double/Float,
String, selected vectors/transforms, NodePath, RID,
PackedStringArray, PackedByteArray, scalar Dictionary, and selected
object/resource handles where lifetime is explicit.
Use typed wrappers when possible. Kanama keeps the broad public Variant API
small because it must define ownership, object lifetime, enum handling, and
generic constraints clearly. This matters for APIs such as
FileAccess.store_var, FileAccess.get_var, Node.rpc_config, and broader
Object/Resource conversions.
Resource Ownership¶
close() means "release my reference", never "destroy this object". It calls
Godot's unreference() and destroys the object only if that dropped the
count to zero. Every rule below falls out of that one sentence: closing is safe
exactly when someone else still holds a reference, and destructive only when you
are the last owner.
Reading a resource off a node and closing it:
val mesh = meshInstance.getMesh() // refcount 2 — the +1 is yours
mesh?.close() // refcount 1 — mesh alive, the node unaffected
getMesh() does not create a new mesh; it hands you the same object with one
more reference. Never closing it is the leak — the count stays at 2 forever and
Godot reports Leaked instance: ArrayMesh at shutdown.
Compare a resource you created and never handed off:
val material = StandardMaterial3D.create() // refcount 1 — you are the ONLY owner
material.close() // refcount 0 — destroyed; later use is a crash
Same call, different outcome, because the number of other owners differs.
Which category is it?¶
| Category | What it covers | What to do |
|---|---|---|
| Owned | X.create(), ResourceLoader.load…, every RefCounted-typed method return including plain getters, and @ScriptProperty reads of resource-typed fields and collections |
close() it, or use { } |
| Borrowed view | A wrapper you mint around a handle you already have: Resource.fromHandle(...), Resource.fromObject(...), a script-class constructor wrapping an existing handle |
Never close() — it releases a reference you never took |
| Engine-owned, live | A createTween() still running, anything assigned into the scene tree, a resource handed to a sink that took its own reference |
Use the Godot lifecycle (kill(), queueFree()), not close() |
Nodes and plain Objects |
Anything not RefCounted — no refcount exists, and GodotObject has no close() |
Node.queueFree() |
The awkward-looking case — a getter you must close — is the common one, and it is safe precisely because the node still holds its own reference.
@ScriptProperty reads are owned: the generated registrar takes its own
reference when it reads a resource out of a property, an Array, or a
Dictionary, and releases it when Godot frees the script instance. You do not
need to close a property field you keep; you do close a temporary you read out
of one and discard.
Two cleanups in one function¶
Nodes are not reference-counted, so close() does not apply to them at all.
Saving part of a scene therefore needs two different cleanups in the same
function — one per category:
val scene = PackedScene.create() // refcount 1 — you are the only owner
scene.pack(someNode) // serializes someNode and its OWNED sub-nodes
ResourceSaver.save(scene, "user://thing.tscn")
scene.close() // refcount 0 — released; the file is already written
someNode.queueFree() // a Node: no refcount, free it through Godot
ResourceSaver.save does not take ownership of the scene — the wrapper is still
live after it returns, which is what makes close() yours to call (and what
issue #81 got wrong; scripts/runtime_smoke.sh asserts the refcount and liveness
after save precisely to keep that fixed).
Two traps worth knowing:
pack()serializes only owned sub-nodes. A node you built and parented but never gave an owner (child.setOwner(root)) saves as a bare root with nothing under it.queueFree()is the only exposed free path for nodes —Object.free()is not wrapped, deliberately.
Temporaries handed to a node¶
When you load a temporary resource and hand it to a
Godot node, release the temporary wrapper with use or close after the node
has accepted the value:
ResourceLoader.loadAudioStream("res://sounds/jump.ogg")?.use { stream ->
player.setStream(stream)
}
This mirrors GDScript's stream = load("res://sounds/jump.ogg"): the node
keeps its own reference, while the local temporary reference is released when
the assignment is done. Kotlin/JVM does not have GDScript's deterministic local
reference cleanup, so relying on garbage collection can leave extra Godot
references alive until shutdown.
Do not register RefCounted values as engine singletons. Godot 4.7 preview builds
warns for Engine.register_singleton with RefCounted instances because the
engine singleton table stores a raw Object*, not a Ref<>. Kanama's
Engine.registerSingleton wrapper rejects this shape before calling Godot; use
an Object-derived singleton instead.
Prefer convenience APIs when they exist. For audio players, use:
player.setStreamFromPath("res://sounds/jump.ogg")
player.play()
The same rule applies to other returned RefCounted helpers, such as
Tween.tweenProperty(...) and Tween.tweenCallback(...): if you do not keep
the returned wrapper, close it after configuring it.
Resource-returning getters follow the same rule. For example,
TextureRect.texture, Sprite2D.texture, and AudioStreamPlayer.getStream()
return closeable wrappers. If you only need to inspect the value, close the
temporary wrapper immediately:
val current = crosshair.texture // refcount 2 — the +1 is yours
check(current != null)
current?.close() // refcount 1 — the node keeps its own
For gameplay animation, prefer node.createTween() over
SceneTree.createTween() when the tween belongs to a node. Godot binds tweens
created this way to the node, so they stop processing when the node leaves the
tree and are killed when the node is freed. Use SceneTree.createTween() only
for tweens that intentionally outlive any particular node, or bind them
explicitly with Tween.bindNode(node).
For @ScriptProperty fields, Kanama-generated registrars release closeable
property wrappers when Godot frees the script instance. Mutable script
properties also release the previous closeable wrapper before accepting a new
value from Godot. This covers retained exported resources such as
PackedScene?, Texture2D?, and List<Texture2D>. List<String> script
properties export as Array[String] and read back as Kotlin strings, including
scene-authored PackedStringArray values.
Inspector Properties and Signals¶
This page focuses on the Godot API wrapper surface. For inspector-visible script data, see Exports and Resources. For custom signals, scene connections, and lambda callbacks, see Signals and Callbacks.
Global Classes¶
Kanama supports globally named classes with @GlobalClass or @ClassName.
@ScriptClass(attachTo = "Node")
@GlobalClass
class Player(godotObject: MemorySegment) :
KanamaScript<Node>(godotObject, ::Node)
This is the Kotlin equivalent of Godot's named script/global class concept and is intended to make classes easier to find in editor-facing workflows.
Use conservative, case-sensitive file/class naming for global classes. Kanama supports the generated metadata path today; editor-facing global-class behavior and typed exported global-class references are advanced usage until the surrounding object/Variant lifetime rules are broader.
Coverage References¶
Use API Coverage and Wrapper Generator Report as the source of truth for promoted wrapper availability. Dynamic container APIs, ownership-sensitive object returns, and convenience helpers are promoted when their Kotlin surface is explicit enough to be stable.