Skip to content

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:

  • PackedStringArray is exposed as List<String> in wrappers such as DirAccess.getFilesAt, ResourceLoader.listDirectory, and ProjectSettings.getChangedSettings. Scene-authored PackedStringArray and Array[String] values also decode to List<String> when they pass through generic Variant paths such as @ScriptProperty setters.
  • PackedByteArray is exposed as ByteArray in FileAccess byte helpers.
  • Selected object arrays are exposed as non-owning Kotlin wrapper lists, such as Area3D.getOverlappingBodies() and Area3D.getOverlappingAreas().
  • Scalar Dictionary values are exposed as Map<String, Any?> where the wrapper knows the dictionary shape, such as ProjectSettings and selected singleton metadata calls. Exported @ScriptProperty maps additionally register as typed Dictionary slots — see Exporting Dictionaries.
  • Vector2 and Vector3 include Kotlin-side arithmetic and common gameplay math helpers such as length, normalized, dot, lerp, distance, and length limiting; Vector3 also includes cross and rotated.
  • Basis and Transform3D expose Godot-style 3D transform math: basis * vector, transform * vector, basis.determinant(), and basis.inverse(). Basis.x, Basis.y, and Basis.z match 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.