Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions config/tomorrow-noon.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
.perl .hljs-sub,
.javascript .hljs-title,
.coffeescript .hljs-title {
color: #81a2be;
color: #025598;
}

/* Tomorrow Purple */
Expand All @@ -83,7 +83,7 @@
.javascript .hljs-function {
/* color: #b294bb;*/
/*color: #02517b;*/
color: #024b86
color: #024b86;
}

/* bromeon: proc-macro attributes */
Expand Down
169 changes: 120 additions & 49 deletions src/register/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- toc -->
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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);
});
}
}
Expand All @@ -175,19 +186,19 @@ 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<T>` as first argument:
If the handler function should run on an object other than `self`, you can use `connect_other()`, which takes a `&Gd<T>` as first argument:

```rust
#[godot_api]
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<Gd<Shield>>`.
// &*self.shield is thus `&Gd<Shield>` we need.
// &*self.shield is thus the `&Gd<Shield>` we need.

self.signals()
.damage_taken()
.connect_obj(&*self.shield, Shield::on_damage_taken);
.connect_other(&*self.shield, Shield::on_damage_taken);
}
}
```
Expand Down Expand Up @@ -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
Expand All @@ -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<T>` trait][api-asarg], which follows
engine APIs and provides flexibility in the argument types. For example, `"string"` can be passed for `impl AsArg<GString>`.

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
Expand All @@ -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<Monster>`. 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<Monster>`. 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:

Expand All @@ -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<T>::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<Node> = ...;
let sig = node.signals().tree_entered();

// You can also access it from a derived class.
let node: Gd<Node3D> = ...;
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<T>` 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.
Expand Down Expand Up @@ -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<Monster>) {
let this = self.to_gd(); // Gd<SoundSystem>

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);
});
}
}
Expand All @@ -331,47 +404,45 @@ fn load_map() {

// Notify player that the world around is now loaded.
let player: Gd<Player> = ...;
player.signals().on_world_loaded().emit();
player.signals().world_loaded().emit();
}
```


## 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<T>`
- Receiver parameter: `function(args)`, `method(&mut T, args)`, `method(Gd<T>, 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<T> (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<Self>, 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
Expand All @@ -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:
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/register/virtual-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Hence the emphasis on "script-virtual".

```admonish note title="Compatibility"
This feature is available from Godot 4.3 onwards.
<sub>(This includes dev and nightly versions after 2024-02-13).</sub>
```


Expand Down
Loading