Skip to content

Registration files and attaching scripts

Godot Kotlin/JVM uses source files for classes declared in the current Godot project and registration files for classes contributed by external dependencies.

  • Project classes are attached through their .kt, .java, or .scala source file.
  • Dependency classes have no source file inside the Godot project. The Gradle plugin generates a .gdj registration file for each usable registered dependency class so it can be attached in the editor.

Source files .kt, .java and .scala

Just like GDScript, you can directly attach Kotlin, Java, and Scala files from the current Godot project to Nodes as scripts. This is the default representation for project classes.

The limitations are the following:

  • The source file must be inside both the Godot project and a configured Gradle source set.
  • The source file name, without its extension, must match the simple name of the script class.
  • Keep one attachable script class in each source file.
  • The registered class information is available after a successful build. Before then, Godot keeps a best-effort source placeholder.

Use the source file directly for project classes.

Registration files .gdj

For each non-abstract registered class discovered in an external dependency, the build generates a corresponding .gdj registration file. Like source files, these files can be attached to Nodes. They make dependency classes available when their source files are not part of the Godot project:

  • Each dependency class gets its own .gdj, including classes from different modules and libraries.
  • A dependency source file can contribute a separate .gdj for each registered class it contains.
  • Registration files are language agnostic: Kotlin, Java, and Scala dependencies use the same format.

By default, dependency registration files are generated into a folder called gdj in the root of your Godot project.

You can however configure the Godot root and the base directory used for newly created registration files inside your build.gradle.kts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import godot.gradle.GodotLanguage

godot {
    // Optional: limit the initial compile pass to the JVM languages your project actually uses.
    languages.set(setOf(GodotLanguage.KOTLIN, GodotLanguage.JAVA))

    // Only needed when the Gradle project directory is not the Godot project root.
    godotProjectDirectory.set(file(".."))

    registrationFilesDirectory.set(<folder>)
}

During the sync step, the Gradle plugin scans the configured Godot project for existing dependency .gdj files. Matching files are updated in place, obsolete ones are deleted, and only newly discovered dependency registrations are copied into registrationFilesDirectory.

Reason

Contrary to GDScript, Kotlin is a compiled language. Hence, if you use a library which defines scripts you can not attach those to nodes anymore as the source files don't exist. You only have a jar of the library. While in GDScript you still have the sources when using an addon. With our registration files our compiler plugin is able to extract those from the libraries you use and provide them to you, so you can also attach scripts from libraries you use.

Class and member registration

Unlike GDScript or C#, JVM declarations pass through a registration step before Godot can use them. The default behavior follows registration annotations and their implied meaning, which keeps the Godot-facing boundary intentional without requiring every annotation to be repeated.

See the registration guide for the complete rules, or continue with the guides for classes, properties, and functions.

Instance types and singletons

Creating a new instance of a Godot type can be done like any JVM object.

1
2
val node3D = Node3D()
val vec = Vector3()
1
2
Node3D node3D = new Node3D();
Vector3 vec = new Vector3();
1
2
val node3D = new Node3D()
val vec = new Vector3()

Godot's singletons are exposed as static access points.

1
Physics2DServer.areaGetTransform(area)
1
Physics2DServer.areaGetTransform(area);
1
Physics2DServer.areaGetTransform(area)

Core types

Godot's built-in types are passed by value (except for Dictionary and VariantArray - more on this later), so the following snippet won't work as expected.

1
2
val node3D = Node3D()
node3D.rotation.y += 10f
1
2
Node3D node3D = new Node3D();
node3D.getRotation().setY(node3D.getRotation().getY() + 10f);
1
2
val node3D = new Node3D()
node3D.getRotation.setY(node3D.getRotation.getY + 10f)

You are actually mutating a copy of the rotation property, not a reference to it. To get the desired behaviour you have to re-assign the copy back.

1
2
3
val rotation = node3D.rotation
rotation.y += 10f
node3D.rotation = rotation
1
2
3
Vector3 rotation = node3D.getRotation();
rotation.setY(rotation.getY() + 10f);
node3D.setRotation(rotation);
1
2
3
val rotation = node3D.getRotation
rotation.setY(rotation.getY + 10f)
node3D.setRotation(rotation)

This approach introduces a lot of boilerplate, so this binding provides a concise way of achieving the same behaviour. Only in Kotlin

1
2
3
node3D.rotationMutate {
  y += 10f
}

The snippet above is functionally equivalent to the previous one.

Collection types

While VariantArray and Dictionary are passed by reference, the value returned by the retrieval methods (VariantArray.get(...) and Dictionary.get(...)) are not.

1
2
array[index].y += 10f
dictionary["foo"].y += 5f
1
2
array.get(index).setY(array.get(index).getY() + 10f);
dictionary.get("foo").setY(dictionary.get("foo").getY() + 5f);
1
2
array.get(index).setY(array.get(index).getY + 10f)
dictionary.get("foo").setY(dictionary.get("foo").getY + 5f)

To get the desired behaviour, you can re-assign the copy back or in a similar fashion as before, this binding provides a better alternative.

1
2
3
4
5
6
7
array.get(index) {
  y += 10f
}

dictionary.get("foo") {
  y += 5f
}
1
2
3
4
5
6
7
Vector3 arrayValue = array.get(index);
arrayValue.setY(arrayValue.getY() + 10f);
array.set(index, arrayValue);

Vector3 dictionaryValue = dictionary.get("foo");
dictionaryValue.setY(dictionaryValue.getY() + 5f);
dictionary.set("foo", dictionaryValue);
1
2
3
4
5
6
7
val arrayValue = array.get(index)
arrayValue.setY(arrayValue.getY + 10f)
array.set(index, arrayValue)

val dictionaryValue = dictionary.get("foo")
dictionaryValue.setY(dictionaryValue.getY + 5f)
dictionary.set("foo", dictionaryValue)

Enums and constants

Godot enums are mapped to Kotlin enums, the generated enum exposes a value property that represents the value in Godot. Constants in Godot classes that represent an enum value (such as Node.PAUSE_MODE_INHERIT) are not present in this module, please use the generated enum instead (Node.PauseMode.INHERIT).

Renamed symbols

To avoid confusion and conflict with Kotlin types, the following Godot symbol is renamed.

  • Array -> VariantArray (to avoid confusion with a built-in type in Kotlin)
  • PackedXArray::toByteArray -> PackedXArray::toPackedByteArray (to avoid confusion with a built-in type in Kotlin)
  • PackedByteArray::toXArray -> PackedByteArray::toPackedXArray (to avoid confusion with a built-in type in Kotlin)
  • All enum values are shortened, the name of the enum itself has been removed. Here are some examples:
  • Error.ERR_PARAMETER_RANGE_ERROR -> Error.PARAMETER_RANGE
  • MethodFlags.METHOD_FLAG_NORMAL -> MethodFlags.NORMAL
  • ProcessThreadMessages.FLAG_PROCESS_THREAD_MESSAGES_PHYSICS -> Error.FLAG_PHYSICS

Global functions

In GDScript, some functions are always available (such as mathematical or RNG functions). The complete list can be found on the following page of Godot's documentation.

In Kotlin, Java, and Scala, global functions are available through the GD singleton helpers. However, don't forget that some functions couldn't be reproduced exactly on the JVM side. For example, load() is available but preload() is not.

Additional functions

For comfort, some objects got some additional functions to enjoy some Kotlin syntax sugar. You can find them all in this folder.

Notifications

Register notification handlers with @Notification.

Unlike GDScript and C++, you do not override _notification directly. Instead, each handled notification is a regular zero-argument method annotated with the notification number it handles.

If several methods in the class hierarchy handle the same notification, they are all called. Normal notification delivery follows Godot's inheritance order, from parent to child. Reversed notification delivery calls them from child to parent.

1
2
3
4
5
6
7
import godot.annotation.Notification
import godot.api.Node

@Notification(Node.NOTIFICATION_READY.toInt())
fun onReadyNotification() {
    // ...
}
1
2
3
4
5
6
7
import godot.annotation.Notification;
import godot.api.Node;

@Notification((int) Node.NOTIFICATION_READY)
public void onReadyNotification() {
    // ...
}
1
2
3
4
5
6
7
import godot.annotation.Notification
import godot.api.Node

@Notification(Node.NOTIFICATION_READY.toInt)
def onReadyNotification(): Unit = {
    // ...
}

Notification handlers are not registered as callable Godot methods. In explicit and inferred registration modes, @Notification is enough to select the method. In automatic mode, a method only becomes a notification handler when the annotation is present.

StringName and NodePath

Several Godot functions take StringName or NodePath as a parameter. It's often more convenient to directly use a String and convert it.

This kind of operation can be costly so we provide extension functions which cache the result of the conversion for later calls:

1
2
3
    val stringName = "myString".asCachedStringName() // Cache the string for faster future calls.
    val nodePath = "myNode/myChildNode".asCachedNodePath() // Cache the string for faster future calls.
    val snakeCaseStringName = "myString".toGodotName() // Convert the string to snake_case and cache it for faster future calls.
1
2
3
    StringName stringName = StringNames.asCachedStringName("myString");
    NodePath nodePath = NodePaths.asCachedNodePath("myNode/myChildNode");
    StringName snakeCaseStringName = StringNames.toGodotName("myString");
1
2
3
    val stringName = StringNames.asCachedStringName("myString")
    val nodePath = NodePaths.asCachedNodePath("myNode/myChildNode")
    val snakeCaseStringName = StringNames.toGodotName("myString")

You can also use the non-cached version of them if you simply want ease of conversion:

1
2
    val stringName = "myString".asStringName()
    val nodePath = "myNode/myChildNode".asNodePath()
1
2
    StringName stringName = StringNames.asStringName("myString");
    NodePath nodePath = NodePaths.asNodePath("myNode/myChildNode");
1
2
    val stringName = StringNames.asStringName("myString")
    val nodePath = NodePaths.asNodePath("myNode/myChildNode")

Logging

If you want logs to appear both in CLI and in the Godot Editor you will have to use the print functions inside the GD singleton like:

1
GD.print("Hello There!")
1
GD.print("Hello There!");
1
GD.print("Hello There!")

Kotlin's print functions, on the other hand, will only print to CLI! They won't print to Godot editor's output panel.