Game classes¶
A game class is a plain Scala class that extends a Godot engine class. The
build discovers it and registers it with Godot's ClassDB, so it can be used
anywhere a built-in class can: scenes, the Add Node dialog, GDScript, and
ClassDB.instantiate.
Imports¶
import gdext.classes.Node2D // engine classes (one file per Godot class)
import gdext.builtin.* // Vector2, Color, Arr, Dict, ...
import gdext.annotations.* // @func, @signal, @gdexport, ...
import gdext.api.* // Gd, Tres, Tscn, ExportHint, emitSignal, Math
These four packages are the public surface. Everything under gdext.internal
is an implementation detail and can change without notice.
What gets registered¶
The sbt-godot-scala-native plugin scans src/main/scala on every compile
and registers a class when all of these hold:
- It is a concrete
class(not atrait,object, orabstract class). - Its parent chain reaches a Godot engine class, either directly or through another game class.
- Godot can construct it with no arguments: every primary-constructor parameter
is a
varor has a default value.
There is no registration list or entry-point file to edit. Adding a class registers it, and deleting one unregisters it.
No case classes
A Godot class cannot be a case class. Godot objects have identity, so the
structural equals, hashCode, and copy that a case class generates
would be wrong for them. If a case class extends an engine class, directly
or through another game class, the build fails and names its file and line.
Write a plain class instead. Case classes that do not extend an engine
class, such as ordinary game data, are fine.
Why discovery happens at build time
Scala Native removes unreferenced classes and never runs module
initializers at load. Classes therefore cannot register themselves at
runtime. The build generates a GeneratedRegistrations source under
target/ that references each class, which keeps it linked and registers it.
Engine virtuals¶
Override an engine virtual and it is wired up automatically:
| Virtual | Signature |
|---|---|
_ready |
(): Unit |
_process |
(delta: Double): Unit |
_physics_process |
(delta: Double): Unit |
_enter_tree / _exit_tree |
(): Unit |
_input / _unhandled_input / _shortcut_input / _unhandled_key_input |
(event: InputEvent): Unit |
Only overridden virtuals are registered, so a class pays nothing for the virtuals it leaves alone.
Calling engine methods¶
Engine methods are camelCase Scala methods on the inherited class, so you call
them directly on this:
Methods that return an engine object return its typed wrapper, or null when
there is none. Use cast to narrow it to a subclass (null if the object is not
one):
Each engine object has exactly one wrapper, so the same node always comes back
as the same Scala object. Methods returning a RefCounted (getMesh,
ResourceLoader.load, ...) hand you a reference to release; see
Memory and object lifetimes.
Creating objects¶
val mesh = Gd.create[ArrayMesh]() // engine class
val enemy = Gd.create[Enemy](Some(this)) // game class, added as a child
Gd.create works for engine and game classes alike. A reference-counted class
(Resource and its subclasses) comes back owning its first reference: release
it with unref() or create it inside Gd.scoped. When you pass a parent
Node, the new node is added as its child.
Logging¶
Gd.print("shown in the Output panel and on stdout")
Gd.printWarning("shown in the debugger's Errors tab")
Gd.printError("shown in the debugger's Errors tab")
Inheriting from another game class¶
A game class can extend another game class. Godot then sees the child as deriving from the parent and exposes the parent's properties, methods, and signals on it.
class Enemy(var hp: Int, var projectile: Option[Projectile], var scene: Tscn[Player])
extends Node2D {
@func def remainingHealth(): Long = hp.toLong
@signal def died(): Unit = ()
}
class Skeleton(
var skeletonName: String = "Bonechill",
hp: Int = 10,
var age: Int
) extends Enemy(hp, None, Tscn.unassigned[Player]) {
override def _ready(): Unit = {
super._ready()
Gd.print(s"Skeleton $skeletonName")
}
}
- The base must be an open
class, notfinal. - The child forwards the base's constructor arguments in its
extendsclause. Its own parameters must still all bevaror defaulted. - Parents are registered before children automatically.
Name clashes with engine members
A property or method whose snake_case name matches an existing engine member
collides with it. For example, a String property called name clashes with
Node.name. The same applies to a @func getHp next to an exported hp,
because Godot already generates get_hp for that property. Pick distinct
names.