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

1

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.