diff --git a/config/tomorrow-noon.css b/config/tomorrow-noon.css index 785d289..52206e1 100644 --- a/config/tomorrow-noon.css +++ b/config/tomorrow-noon.css @@ -74,7 +74,7 @@ .perl .hljs-sub, .javascript .hljs-title, .coffeescript .hljs-title { - color: #81a2be; + color: #025598; } /* Tomorrow Purple */ @@ -83,7 +83,7 @@ .javascript .hljs-function { /* color: #b294bb;*/ /*color: #02517b;*/ - color: #024b86 + color: #024b86; } /* bromeon: proc-macro attributes */ diff --git a/src/register/signals.md b/src/register/signals.md index 418b5cb..e6b875b 100644 --- a/src/register/signals.md +++ b/src/register/signals.md @@ -12,6 +12,16 @@ Signals are a Godot mechanism to implement the Observer pattern. You can emit ev read the [GDScript tutorial][godot-gdscript-signals]. +```admonish note title="Compatibility" +Typed signals require at least Godot 4.2 and godot-rust v0.3. +`#[signal]` for registration alone is available before. +``` + +```admonish info title="New API" +Typed signals are a major new feature in v0.3. The API may still change in the future, and some parts are not yet implemented. +``` + + ## Table of contents @@ -71,10 +81,10 @@ this interface contract, apart from a high level of manual discipline and testin ## Rust signals -godot-rust provides a type-safe and straightforward API to connect and emit signals, even though they are untyped in GDScript. +godot-rust provides a type-safe and straightforward API to connect and emit signals, even though the latter are untyped in GDScript. You can rely on signatures and don't need to fear refactorings, as Rust will catch any mismatches at compile time. -In Rust, signals can be defined with the `#[signal]` attribute inside a `#[godot_api]` block. +In godot-rust, signals can be defined with the `#[signal]` attribute inside a `#[godot_api]` block. Let's take again our class from earlier and declare a `damage_taken` signal: ```rust @@ -98,7 +108,7 @@ are not supported. ### Generated code -As soon as you register at least one signal, godot-rust will implement the [`WithSignals`][api-withsignals] trait for your class. +As soon as you register at least one signal, godot-rust will implement the [`WithUserSignals`][api-withusersignals] trait for your class. This provides the `signals()` method, which can now be accessed inside class methods. `signals()` returns a _signal collection_, i.e. a struct which exposes all signals as named methods: @@ -162,11 +172,12 @@ impl INode3D for Monster { fn ready(&mut self) { self.signals() .damage_taken() - .connect_self(|this: &mut Self, amount| { - // ^^^^^^^^^ - // must be explicit; other parameters types are inferred. + .connect_self(|this, amount| { + // ^^^^ ^^^^^^ + // types inferred as &mut Self, i32 - ... // Update healthbar, play sound, etc. + this.update_healthbar(amount); + this.play_sound(Sfx::MonsterAttacked); }); } } @@ -175,7 +186,7 @@ impl INode3D for Monster { ### Handler on different object -If the handler function should run on an object other than `self`, you can use `connect_obj()`, which takes a `&Gd` as first argument: +If the handler function should run on an object other than `self`, you can use `connect_other()`, which takes a `&Gd` as first argument: ```rust #[godot_api] @@ -183,11 +194,11 @@ impl INode3D for Monster { fn ready(&mut self) { // Let's say damage is deflected to a shield object. // That one is stored as field `shield: OnReady>`. - // &*self.shield is thus `&Gd` we need. + // &*self.shield is thus the `&Gd` we need. self.signals() .damage_taken() - .connect_obj(&*self.shield, Shield::on_damage_taken); + .connect_other(&*self.shield, Shield::on_damage_taken); } } ``` @@ -226,7 +237,7 @@ impl INode3D for Monster { ## Emitting signals -We already saw that `#[signal]` attributes generate a signal type with several methods: `connect()`, `connect_self()` and `connect_obj()`. +We already saw that `#[signal]` attributes generate a signal type with several methods: `connect()`, `connect_self()` and `connect_other()`. This same signal type also provides an `emit()` method, which you can use to trigger the signal: ```rust @@ -243,7 +254,8 @@ Like `connect*()` methods, `emit()` is fully type-safe. You can only pass a sing `bool` or `enum` value for the type of damage, the compiler will catch all `connect*` and `emit` calls. You'll sleep well after refactorings. The nice thing about `emit()` is that it also comes with parameter names, as provided in the `#[signal]` attribute. This lets IDEs provide -more context, e.g. show parameter inlay hints in `emit()` calls. +more context, e.g. show parameter inlay hints in `emit()` calls. The parameter types use the [`AsArg` trait][api-asarg], which follows +engine APIs and provides flexibility in the argument types. For example, `"string"` can be passed for `impl AsArg`. In addition to the specific `emit()` method, the `TypedSignal` (deref target of the custom signal type) also provides a generic method `emit_tuple()`, which takes a tuple of all arguments, by value. This is rarely needed, but can be useful in situations where you want to pass @@ -257,8 +269,8 @@ self.signals().damage_taken().emit_tuple((amount,)); ## Accessing signals outside the class As your game grows in interactions, you may want to configure or emit signals not just within `impl Monster` blocks, but also from other parts -of your codebase. The trait method [`WithSignals::signals()`][api-withsignals] allows direct access from `&mut self`, but outside you often -only have a `Gd`. You could technically `bind_mut()` that object, but there's a better way without borrow-checking. +of your codebase. The trait method [`WithUserSignals::signals()`][api-withusersignals] allows direct access from `&mut self`, but outside you +often only have a `Gd`. You could technically `bind_mut()` that object, but there's a better way without borrow-checking. For this reason, `Gd` itself [_also_ provides a `signals()` method][api-gd-signals], returning the exact same _signal collection_ API: @@ -268,6 +280,69 @@ let sig = monster.signals().damage_taken(); ``` +### Godot built-in signals + +Godot provides many built-in signals to hook into lifecycles and events. All engine-provided classes implement the +[`WithSignals`][api-withsignals] trait, which is a supertrait of [`WithUserSignals`][api-withusersignals]. + +Every class `T` has its own signal collection, accessible by `Gd::signals()`. Like class methods, signals are inherited, so you can do +the following: + +```rust +// tree_entered is a signal declared on Node. +let node: Gd = ...; +let sig = node.signals().tree_entered(); + +// You can also access it from a derived class. +let node: Gd = ...; +let sig = node.signals().tree_entered(); +``` + +This works also in user-defined classes. This means we can extend our previous `ready()` implementation to connect Godot signals: + +```rust +#[godot_api] +impl INode3D for Monster { + fn ready(&mut self) { + // Previous code. + self.signals() + .damage_taken() + .connect_other(&*self.shield, Shield::on_damage_taken); + + // Connect to the `Node::renamed` signal, which is invoked + // when a node name changes. + self.signals() + .renamed() + .connect_self(|this| { + let new_name = this.base().get_name(); + println!("Monster node renamed to {new_name}."); + }); + } +} +``` + +```admonish tip title="Disabling typed signals" +The generated API for typed signals usually does no harm even if you don't use it. However, it is possible to disable code generation with: +~~~rust +#[godot_api(no_typed_signals)] +impl MyClass { ... } +~~~ + +This still allows you to use `#[signal]` and will register each signal declared as such, but it won't generate a `signals()` collection. +``` + +```admonish note title="Availability of signal API" +The typed signal API is generated for your class under the following conditions: + +- Your class declares a `Base` field. +- You have a `#[godot_api]` block (empty if necessary). + - This is a technical limitation that may be lifted in the future. +- You do not opt out from typed signals with `no_typed_signals`. + +Signals, typed or not, **cannot** be declared in secondary `impl` blocks (those annotated with `#[godot_api(secondary)]` attribute). +``` + + ### Signal visibility Like all items in Rust, signals are private by default, i.e. only visible in their module and submodules. @@ -308,12 +383,10 @@ Let's say you have a sound system which should play a sound effect whenever a mo ```rust impl SoundSystem { fn connect_sound_system(&self, monster: &Gd) { - let this = self.to_gd(); // Gd - monster.signals() .damage_taken() - .connect_obj(this, |s: &mut Self, _amount| { - s.play_sound(Sfx::MonsterAttacked); + .connect_other(self, |this, _amount| { + this.play_sound(Sfx::MonsterAttacked); }); } } @@ -331,7 +404,7 @@ fn load_map() { // Notify player that the world around is now loaded. let player: Gd = ...; - player.signals().on_world_loaded().emit(); + player.signals().world_loaded().emit(); } ``` @@ -339,39 +412,37 @@ fn load_map() { ## Advanced signal setups The `TypedSignal::connect*()` methods are designed to be straightforward, while covering common use cases. If you need more advanced setups, -a high degree of customization is provided by [`TypedSignal::connect_builder()`][api-typedsignal-connectbuilder]. +a high degree of customization is provided by [`TypedSignal::builder()`][api-typedsignal-builder]. -The returned `ConnectBuilder` provides several dimensions of configurability: +The returned [`ConnectBuilder`][api-connectbuilder] provides several dimensions of configurability: -- Receiver: `function(args)`, `method(&self, args)`, `method(&mut self, args)` -- Provided object: none, `&mut self` or `Gd` +- Receiver parameter: `function(args)`, `method(&mut T, args)`, `method(Gd, args)` +- Provided object: none, `self` or other instance - Connection flags: `DEFERRED`, `ONESHOT`, `PERSIST` -- Single-threaded (default) or thread-crossing (_sync_) +- Single-threaded (default) or thread-crossing ("sync") To finish it, `done()` is invoked. Some example setups: ```rust -// Connect -> Self::log_event(&self, event: String) -signal.connect_builder() - .object_self() // pass in &self (the object surrounding the signal) - .method_immut(Self::log_event) // receive &self +// Connect -> Self::log_event(&mut self, event: String) +signal.builder() .flags(ConnectFlags::DEFERRED | ConnectFlags::ONESHOT) - .done(); - -// Connect -> Logger::log_event_mut(&mut self, event: String) -signal.connect_builder() - .object(some_gd) // pass in Gd (arbitrary object) - .method_mut(Logger::log_event_mut) // receive &mut self - .done(); - -// Connect -> Logger::log_event(event: String) -signal.connect_builder() - .function(Logger::log_event) // associated fn, no receiver - .sync() // allows another thread to receive signal (without panic) - .done(); + .connect_self_mut(Self::log_event); // receive &mut self + +// Connect -> Logger::log_event(&mut self, event: String) +signal.builder() + .connect_other_mut(some_gd, Logger::log_event); + +// Connect -> Logger::log_event(this: Gd, event: String) +signal.builder() + .connect_other_gd(some_gd, Logger::log_event); + +// Connect -> Logger::log_thread_safe(event: String) +signal.builder() + .connect_sync(Logger::log_thread_safe); // associated fn, no receiver ``` -The builder methods need to be called in the correct order ("stages"). See [API docs][api-typedsignal-connectbuilder] for more information. +The builder methods need to be called in the correct order ("stages"). See [API docs][api-typedsignal-builder] for more information. ### Untyped signals @@ -383,8 +454,8 @@ Godot's low-level APIs for dealing with untyped signals are still available: - [`Signal::connect()`][api-signal-connect] - [`Signal::emit()`][api-signal-emit] -They can be used as a fallback for areas that the new typed signal API doesn't cover yet (e.g. Godot's built-in signals), or in situations -where you only have some information available at runtime. +The new typed-signal API should cover the full functionality, but there are situations where information is only available at runtime, making +the untyped reflection APIs a good fit. We might also combine the two in the future. To emit an untyped signal, you can call the `Object::emit_signal` method by accessing the base class (mutably): Considering the `Monster` struct from the previous examples, you can emit its signal with: @@ -402,19 +473,19 @@ to be ported to godot-rust, e.g. a `Callable::bind()` equivalent for typed Rust ## Conclusion -In this chapter, we saw how godot-rust's **type-safe signals** provide an intuitive and resilient way to deal with Godot's observer pattern +In this chapter, we saw how godot-rust's **typed signals** provide an intuitive and resilient way to deal with Godot's observer pattern and avoid certain pitfalls of GDScript. Rust function references or closures can be directly connected to signals, and emitting is achieved with regular function calls. -[api-object]: https://godot-rust.github.io/docs/gdext/master/godot/classes/struct.Object.html -[api-signal]: https://godot-rust.github.io/docs/gdext/master/godot/register/derive.GodotClass.html#signals +[api-asarg]: https://godot-rust.github.io/docs/gdext/master/godot/meta/trait.AsArg.html [api-withsignals]: https://godot-rust.github.io/docs/gdext/master/godot/obj/trait.WithSignals.html +[api-withusersignals]: https://godot-rust.github.io/docs/gdext/master/godot/obj/trait.WithUserSignals.html [api-gd-signals]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.Gd.html#method.signals [godot-gdscript-signals]: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#signals [api-typedsignal]: https://godot-rust.github.io/docs/gdext/master/godot/register/struct.TypedSignal.html -[api-typedsignal-connectbuilder]: https://godot-rust.github.io/docs/gdext/master/godot/register/struct.TypedSignal.html#method.connect_builder - +[api-typedsignal-builder]: https://godot-rust.github.io/docs/gdext/master/godot/register/struct.TypedSignal.html#method.builder +[api-connectbuilder]: https://godot-rust.github.io/docs/gdext/master/godot/register/struct.ConnectBuilder.html [api-object-connect]: https://godot-rust.github.io/docs/gdext/master/godot/classes/struct.Object.html#method.connect [api-object-emitsignal]: https://godot-rust.github.io/docs/gdext/master/godot/classes/struct.Object.html#method.emit_signal [api-signal-connect]: https://godot-rust.github.io/docs/gdext/master/godot/builtin/struct.Signal.html#method.connect diff --git a/src/register/virtual-functions.md b/src/register/virtual-functions.md index d11cb60..a913efa 100644 --- a/src/register/virtual-functions.md +++ b/src/register/virtual-functions.md @@ -14,7 +14,6 @@ Hence the emphasis on "script-virtual". ```admonish note title="Compatibility" This feature is available from Godot 4.3 onwards. -(This includes dev and nightly versions after 2024-02-13). ```