Signals and Callables
Unlike GDScript, signals and callables expose a typed API on top of their base dynamic form.
Safe and unsafe APIs
Signal and Callable start from a base typeless, arityless interface.
That base layer exposes the dynamic operations:
Signal.emitUnsafe(...)Signal.connectUnsafe(...)Callable.callUnsafe(...)Callable.callDeferredUnsafe(...)Callable.bindUnsafe(...)
The Unsafe variants are the true equivalent to the dynamic GDScript behavior.
They take a vararg Any? argument list and defer correctness checks to runtime.
That is useful for interoperability with values coming from Godot, where the exact signature is not always known on the JVM side. For regular code, they should generally be avoided in favor of the typed APIs.
On top of that base layer, signals and callables are specialized by arity and generic parameter types:
Signal0toSignal16Callable0toCallable16
These specialized variants expose the safe equivalents of the base operations:
emit(...)instead ofemitUnsafe(...)connect(...)instead ofconnectUnsafe(...)call(...)andinvoke(...)instead ofcallUnsafe(...)- typed
bind(...)instead ofbindUnsafe(...)
Those safe methods take a specific number of typed arguments matching the signal or callable arity. This is where compile-time checking comes from.
One important difference between JVM languages is how much type information can be recovered from the source syntax:
- Kotlin gets the most ergonomic API, with delegates, method references, and lambda helpers such as
signalN,methodCallableN,lambdaCallableN,connectMethod, andconnectLambda. - Java and Scala use the same typed signal and callable families, but usually construct them more explicitly with
SignalN.create(...),MethodCallableN.create(...),MethodStringNameN, andLambdaCallableN.create(...).
How this differs from GDScript
In GDScript, signal and callable usage is mostly checked at runtime:
1 2 3 4 5 6 7 | |
Here, the signal and callable arity is part of the type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Signal2<Int, Int> can only emit two Ints, and it can only connect to a Callable2<*, Int, Int>.
If you call emitUnsafe or connectUnsafe, you are back to the dynamic style used by GDScript.
That is why the safe typed API is the default recommendation.
Warning
Typed signal and callable variants currently go up to 16 parameters.
Declaring signals
The cross-language baseline is the explicit SignalN.create(...) form:
1 2 | |
1 2 | |
1 2 | |
For Java and Scala, this is also the normal declaration style inside a class.
Make sure the variable name and the string passed to SignalN.create(...) are the same.
Use the source-language name such as healthChanged, not a manually converted snake_case version.
The signal is registered to Godot from the variable itself, but the signal instance also needs to carry its own name so Godot can identify it correctly.
The conversion to Godot's snake_case name happens automatically.
Kotlin also provides a delegate syntax, which is usually the recommended form for Kotlin classes:
1 2 3 4 5 | |
This is lightweight.
The delegate does not store a dedicated Signal2 instance on the object.
It recreates a wrapper on access from the owning object and the property name.
That delegate syntax is specific to Kotlin.
Emitting signals
Typed signals expose a typed emit function:
1 | |
1 | |
1 | |
Callable kinds
There is one base Callable interface, and the most useful concrete variants are:
MethodCallableN: wraps a method on a Godot object.LambdaCallableN: wraps a JVM lambda.VariantCallable: wraps Godot's native dynamic callable type.
Method callables
Use method callables when the callback is an existing registered Godot method.
Kotlin and Java/Scala reach that goal differently:
- Kotlin usually uses method references, so
methodCallableN(target, Type::method)is the most natural form. - Java and Scala usually create a
MethodCallableNexplicitly from a method name. - For built-in Godot API methods, Java and Scala should prefer the pre-made
MethodStringNameNfields exposed by engine classes.
Kotlin
1 2 3 4 5 6 7 8 9 10 11 | |
Java and Scala
For built-in Godot API methods, use the pre-made typed method-name fields exposed by engine classes.
If you want the same type-safe path for your own exported methods in Java or Scala, you can create a MethodStringNameN(...) explicitly.
1 2 3 4 | |
1 2 3 4 5 | |
The fallback createUnsafe(target, "methodName") form still exists, but it drops back to string-based runtime checks.
Use it only when you cannot express the callable with a typed helper.
Lambda callables
Use lambda callables when the callback only exists on the JVM side and is not a registered Godot method.
The language split is similar here:
- Kotlin usually uses
lambdaCallableN { ... }or.asCallable(). - Java and Scala use
LambdaCallableN.create(...)and provide explicit JVM classes for arguments and return values.
Kotlin
Create one directly:
1 2 3 | |
Or convert an existing lambda:
1 2 3 | |
Java and Scala
For a no-return callable:
1 2 3 4 5 | |
1 2 3 4 5 | |
For a callable with a return value:
1 2 3 4 5 6 | |
1 2 3 4 5 6 | |
If you expose one of those Java or Scala callables as a registered property, prefer the base Callable type for the property itself.
The stored value can still be a LambdaCallableN, but the property surface should currently stay at Callable.
Variant callables
VariantCallable is the native, fully dynamic callable wrapper.
It is useful when a callable comes from Godot itself and not from the typed APIs shown above.
In user code, prefer the typed families when you know the signature.
Typed callables
Each typed callable exposes:
call(...)invoke(...)callDeferred(...)bind(...)
Example:
1 2 3 4 5 6 7 | |
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 4 5 6 7 8 9 10 11 12 | |
bind(...) always binds arguments from the right and returns a callable with a smaller arity.
Connecting signals
There are two main patterns when connecting a typed signal:
- connect an existing method
- connect an inline lambda
Kotlin has convenience helpers for both.
Java and Scala have arity-specific SignalConnectors.connectMethodX(...) and SignalConnectors.connectLambdaX(...) helpers that pass the typed method-name or JVM action information explicitly.
Connect a method
The most explicit form is signal.connect(callable).
That is the default mental model in Java and Scala, and it also works in Kotlin.
1 2 3 4 5 | |
1 2 3 4 5 | |
1 2 3 4 5 | |
Connect a lambda
For inline subscriptions, connect a lambda callable directly:
1 2 3 4 5 | |
1 2 3 4 5 6 7 | |
1 2 3 4 5 6 7 | |
SignalConnector
SignalConnector is a small helper around one Signal plus one Callable.
It exists for the cases where you want a reusable connection handle instead of just calling signal.connect(callable) directly.
That makes it easy to:
connect()disconnect()isConnected()isValid()
Create one from a method
Kotlin provides connectMethod, which creates the callable, connects it immediately, and returns a SignalConnector:
1 2 3 4 | |
Java and Scala can use the same SignalConnector helper with a pre-made typed method name:
1 2 3 4 5 6 7 8 9 10 11 | |
1 2 3 4 5 6 7 8 9 10 11 | |
Create one from a lambda
For inline subscriptions, Kotlin provides connectLambda:
1 2 3 | |
Under the hood this creates a typed LambdaCallableN, connects it, and returns a SignalConnector.
Java and Scala can also use a direct connector helper:
1 2 3 4 5 6 | |
1 2 3 4 5 6 | |
This avoids having to rebuild the same callable manually later or keep the raw signal/callable pair around yourself.
A complete example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
The Kotlin example is fully type checked against Signal2<Int, Int>.
The Java and Scala versions keep the same explicit signal/callable structure, but their safety depends on which callable construction path you use.
Naming
For consistency with Godot, signals are registered to Godot in snake_case.
For example, a property named healthChanged is exposed to Godot as health_changed.
When you create a signal wrapper manually with SignalN.create(...), pass the original property name.
The wrapper converts it to the Godot name automatically.