May 2025 dev update
godot-rust is ready for summer, with its fresh v0.3 release! This update introduces a few major features, sprinkled with a ton of smaller improvements.
Typed signals
Signals are a core mechanism in Godot's architecture, enabling the Observer pattern for communication between objects.
One problem with signals in GDScript is that they aren't type-checked. Even if you declare them as signal damage_taken(amount: int)
, this is
only informational -- the number and types of arguments aren't verified by the signal1.
Rust, being a language with a focus on safety and robustness, has no excuse to not do this better. So, if you write this...
#[signal]
fn damage_taken(amount: i32);
...you can now do all of this cool stuff:
// Somewhere in your inherent impl:
fn on_damage_taken(&mut self, amount: i32) {
// ...
}
// Your setup code:
fn ready(&mut self) {
// Connect signal to the method:
self.signals().damage_taken().connect(Self::on_damage_taken);
// Or to an ad-hoc closure:
self.signals().damage_taken().connect(|amount| {
println!("Damage taken: {}", amount);
});
// Or to a method of another object:
let stats: Gd<Stats>;
self.signals().damage_taken().connect_other(&stats, |stats, amount| {
stats.update_total_damage(amount);
});
}
Of course, you can also emit the signal in a type-safe way:
self.signals().damage_taken().emit(42);
If you change your #[signal]
declaration, all the connect
and emit
call sites will no longer compile, until you update them.
Fearless refactoring!
For an in-depth tutorial, check out the Signals chapter in the book.
The signal system took several months to evolve and went through many iterations of going back to the drawing board, in more than 15 pull requests. Special thanks to Houtamelo and Yarwin for going through trait and macro hell to make the fluent APIs more user-friendly, as well as the many users who gave feedback during the initial design phase. The API will keep evolving!
Async/await
v0.3 is the first version to introduce the async/await paradigm, thanks to the work of TitanNano in #1043.
Asynchronous programming is provided through signals, which can be awaited in a non-blocking way. This follows the idea of GDScript's own
await
keyword.
To follow the above example with the damage_taken
signal, you can now spawn an async task that waits for the signal to be emitted:
let player: Gd<Player> = ...;
godot::task::spawn(async move {
godot_print!("Wait for damage to occur...");
// Emitted arguments can be fetched in tuple form.
// If the signal has no parameters, you don't need `let`.
let (dmg,) = player.signals().damage_taken().to_future().await;
godot_print!("Player took {dmg} damage.");
});
OnEditor
-- fields initialized in the Godot editor
We already have OnReady<T>
to allow late initialization of fields during ready()
. The new OnEditor<T>
struct extends this
idea to fields that cannot be initialized in neither the init()
constructor nor a ready()
method, but instead need to be set externally
in the Godot editor.
In particular, using Gd<T>
with #[export]
consistently caused problems with initialization, and is now substituted by OnEditor<Gd<T>>
.
Interface traits + final classes
Interface traits like INode3D
, IResource
etc. have been ubiquitous in godot-rust. You come across them whenever you implement a constructor
or override virtual functions. Incidentally, we found that 118 of these traits were effectively dead code, because Godot doesn't allow inheriting
their respective classes. Examples include IFileAccess
, IIp
, IScript
and many more. We removed them all, thus moving runtime errors to
compile time.
Additionally, non-inheritable classes in Godot are now properly marked as such in the docs, we currently use the term "final class" for this.
There are now descriptive compile-time errors when attempting #[class(base = FileAccess)]
.
Usability
process()
and physics_process()
both require a delta: f64
parameter. Many Godot APIs (sound, timers, etc.) use f64
, while many others
(vectors, matrices, etc.) use f32
in default builds. This caused minor but steady friction with extra as
casts.
Now, you can additionally override process()
and physics_process()
with delta: f32
. This is transparently converted from f64
by the
proc-macro. Details are available in #1110.
bind()
and bind_mut()
have caused lots of runtime borrow errors. Fear not: they will keep doing exactly that. However, with
RUST_BACKTRACE=1
, you can now retrieve the exact call stacks where the problem occurs. Not just once it panics, but retroactively where
the previous borrow originated! Check out #1094.
String types (GString
, StringName
, NodePath
) now support loading from &[u8]
and &CStr
, with specified text encoding.
This bridges the gap to low-level GDExtension APIs, with additional validation on top.
When loading resources into fields, there is a new attribute #[init(load = "PATH")]
, which calls OnReady::from_loaded()
,
which again calls load()
and stores the result in the OnReady
.
Generated docs for the editor now support @experimental
and @deprecated
attributes, and will be displayed as such in the Godot editor.
Project structure improvements
Examples were moved into a separate repo demo-projects, with their own issue management and continuous integration.
API docs now mention (again) if a feature is only available from a certain Godot version, or if it is gated behind experimental-godot-api
.
In the library, we got rid of two dependencies and reduced the "minimum codegen" set, which reduces compile times, especially in CI. Lots of smaller refactorings have happened to keep the development process as smooth as possible.
That's it for now!
We hope you enjoy the improvements since the 2024 review, and wish you great success in your projects!
Whether those are games, plugins or other software, join our Discord and let us know how you use the library in #showcase
!
As always, the complete list of changes is available in the changelog. See also our v0.2 -> v0.3 migration guide and recently merged pull requests.
Footnotes
Arguments may be checked by the receiving function (connected to the signal), depending on its signature, and only
at runtime. However, the signal
itself doesn't verify this.
See also the last 🛈 Note box here.