Skip to content

Commit 69b5c45

Browse files
committed
Devlog article: November update
1 parent 51179c6 commit 69b5c45

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
+++
2+
# Copyright (c) godot-rust; Bromeon and contributors.
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
title = "November 2024 dev update"
8+
authors = ["Bromeon"]
9+
10+
[extra]
11+
summary = "v0.2 release, ergonomic arguments, RPC methods, node init, ..."
12+
tags = ["dev-update"]
13+
+++
14+
15+
Things have been slightly calmer in the last few months, yet a lot has happened behind the scenes!
16+
17+
This post announces the next big iteration of godot-rust, version **0.2**. We are excited to share what this release brings to the table.
18+
19+
20+
## Ergonomic + fast argument passing
21+
22+
In version 0.1, all Godot engine APIs took their arguments by value, with a concrete parameter type. This approach has notable drawbacks:
23+
24+
1. It often requires conversions because your argument type doesn't match the declared parameter exactly.
25+
- Passing strings as `"string".into()` has become its own idiom.
26+
- If a method accepts `Gd<Node>` but you have a `Gd<Node2D>`, you'll need to call `node.upcast()`.
27+
28+
2. If you want to keep using your (non-`Copy`) argument after the call, you _must_ use `.clone()` all the time.
29+
- It makes code repetitive and attracts unneeded attention. In Rust, `clone()` is not _that_ common for passing arguments.
30+
- Creating such a clone army has a performance cost for reference-counted types (`Gd<RefCounted>`, `GString`, `Array`, ...). If Godot takes ownership of a value (e.g. storing a string), it already clones internally, so you pay twice.
31+
32+
33+
### Powerful conversions
34+
35+
In version 0.2, we introduce a streamlined API for argument passing. Affecting the core type machinery, this took a ludicrous amount of time to implement, but is (hopefully...) worth the results:
36+
37+
1. **Pass by reference.**
38+
39+
All container types such as `Gd`, `Array`, `Dictionary`, `GString`, `Callable`, `Variant`, `PackedInt32Array` are now passed by reference. This means you get rid of an unnecessary clone, you can keep using the value after the call, and your call-site code is often reduced to a single `&` borrow.
40+
41+
Types which implement `Copy`, such as `i32`, `bool`, `Vector3`, `Color` etc. are still passed by value.
42+
43+
2. **Automatic upcasting.**
44+
45+
If a parameter expects an object parameter of class `T`, you can now not only pass `T` objects, but instances of all classes that inherit from `T`. You no longer need a manual `.upcast()` call, the library takes care of this, completely type-safe.
46+
Rust is clearly a very OOP language.
47+
48+
3. **Implicit string conversions.**
49+
50+
Rust is following the "make things explicit" idea in a hardcore way, and in many cases this prevents errors and makes code easier to read. But there are situations where this results in verbosity before anything else, becoming a burden on your code -- especially in game development where fast prototyping matters.
51+
52+
`"some_string".into()` is a good example of this. After certain time, you'll _know_ that Godot has its own string types different from Rust `&str`, so the fact that a conversion is happening is no longer providing you valuable information -- at least not to the point where you want to be reminded of it in every 2nd line of code.
53+
54+
This is why you can now pass `&str` strings as `"some_string"` literals directly. If you have `String` instances, just borrow them with `&my_string`.
55+
56+
57+
### Talk is cheap, show me the code
58+
59+
These are real code samples from the library's integration tests and the dodge-the-creeps demo.
60+
Get your own impression of the before/after:
61+
62+
```rust
63+
// BEFORE: strings always converted with .into().
64+
message_label.set_text("Dodge the\nCreeps!".into());
65+
let val: Array<GString> = array!["Godot".into(), "Rust".into(), "Rocks".into()];
66+
67+
// AFTER: strings can be passed directly, even in array literals.
68+
message_label.set_text("Dodge the\nCreeps!");
69+
let val: Array<GString> = array!["Godot", "Rust", "Rocks"];
70+
```
71+
72+
```rust
73+
// BEFORE: test code needs to clone arg on each call.
74+
let changes = StringName::from("property_changes");
75+
assert!(!revert.property_can_revert(changes.clone()));
76+
assert!(revert.property_can_revert(changes.clone()));
77+
assert_eq!(revert.property_get_revert(changes.clone()), Variant::nil());
78+
79+
// AFTER: just borrow it.
80+
let changes = StringName::from("property_changes");
81+
assert!(!revert.property_can_revert(&changes));
82+
assert!(revert.property_can_revert(&changes));
83+
assert_eq!(revert.property_get_revert(&changes), Variant::nil());
84+
```
85+
86+
```rust
87+
// BEFORE: not only cloning, but upcasting.
88+
self.base_mut().add_child(mob_scene.clone().upcast());
89+
90+
// AFTER: auto-upcast, no clone.
91+
self.base_mut().add_child(&mob_scene);
92+
```
93+
94+
These changes have been implemented in a marathon of PRs (where an addition typically required 3 follow-up PRs to fix the fallout):
95+
- Object parameters: [#800], [#823], [#830], [#846]
96+
- Pass-by-ref: [#900], [#906], [#947], [#948]
97+
- String conversions: [#940]
98+
<sup>(no follow-up here is admittedly suspicious...)</sup>
99+
100+
[#800]: https://github.com/godot-rust/gdext/pull/800
101+
[#823]: https://github.com/godot-rust/gdext/pull/823
102+
[#830]: https://github.com/godot-rust/gdext/pull/830
103+
[#846]: https://github.com/godot-rust/gdext/pull/846
104+
[#900]: https://github.com/godot-rust/gdext/pull/900
105+
[#906]: https://github.com/godot-rust/gdext/pull/906
106+
[#940]: https://github.com/godot-rust/gdext/pull/940
107+
[#947]: https://github.com/godot-rust/gdext/pull/947
108+
[#948]: https://github.com/godot-rust/gdext/pull/948
109+
110+
111+
## Path-based node initialization
112+
113+
In [#807], Houtamelo added a great feature: initialization for nodes based on a path. This was achieved by wiring up `OnReady<T>` with custom init logic, exposed through a new `#[init(node)]` attribute.
114+
115+
The following code directly initializes fields with the nodes found at the given path:
116+
```rust
117+
#[derive(GodotClass)]
118+
#[class(init, base=Node3D)]
119+
struct Main {
120+
base: Base<Node3D>,
121+
122+
#[init(node = "Camera3D")]
123+
camera: OnReady<Gd<Camera3D>>,
124+
125+
#[init(node = "Hud/CoinLabel")]
126+
coin_label: OnReady<Gd<Label>>,
127+
}
128+
```
129+
130+
In case you don't know [`OnReady`][api-onready], it provides a late-init mechanism with ergonomic access, i.e. no constant `.unwrap()` or defensive if-initialized checks. You can access `OnReady<Gd<Node>>` as if it were a `Gd<Node>`:
131+
132+
```rust
133+
self.coin_label.set_text(&format!("{} coins", self.coins));
134+
```
135+
136+
[#807]: https://github.com/godot-rust/gdext/pull/807
137+
138+
139+
## Generating Godot docs from RustDoc
140+
141+
[#748] is a pull request from bend-n, which adds another great feature: the ability to register documentation alongside Rust classes and methods. If you enable the `register-docs` crate feature, you can use regular RustDoc comments, which will be picked up by the editor.
142+
143+
Let's say you have the following Rust code. It registers a class with a property and a function, all of which are documented:
144+
145+
146+
```rust
147+
/// A brief description on the first line.
148+
///
149+
/// Link to a **Godot** type [AABB].
150+
/// And [external link](https://godot-rust.github.io).
151+
///
152+
/// ```gdscript
153+
/// # Syntax highlighted.
154+
/// extends Node
155+
///
156+
/// @onready var x: Array[int]
157+
///
158+
/// func _ready():
159+
/// pass
160+
/// ```
161+
#[derive(GodotClass)]
162+
#[class(init, base=Node)]
163+
struct DocExample {
164+
/// Property is _documented_.
165+
#[export]
166+
integer: i32,
167+
}
168+
169+
#[godot_api]
170+
impl DocExample {
171+
/// Custom constructor takes `integer`.
172+
#[func]
173+
fn create(integer: i32) -> Gd<Self> {
174+
Gd::from_object(DocExample { integer })
175+
}
176+
}
177+
```
178+
179+
This will render as follows in the editor:
180+
181+
![godot-rust docs in Godot](register-docs.png)
182+
183+
This even works with editor hot-reloads (although you need to reopen the doc tab).
184+
Not all Markdown elements are supported yet, but this will improve over time. Contributions are of course welcome!
185+
186+
[#748]: https://github.com/godot-rust/gdext/pull/748
187+
188+
## `#[rpc]` attribute
189+
190+
Houtamelo also helped build [#902], a PR which adds an `#[rpc]` attribute to user-defined functions. This brings the [GDScript `@rpc`][gdscript-rpc] feature to Rust, allowing you to configure remote procedure calls in your Rust scripts.
191+
192+
Example usage:
193+
```rust
194+
#[rpc(any_peer, reliable, call_remote, channel = 3)]
195+
fn my_rpc(&self, i: i32, s: String) -> Variant {
196+
...
197+
}
198+
```
199+
You can also define a global RPC configuration and reuse it for multiple functions:
200+
```rust
201+
const CONFIG: RpcConfig = RpcConfig {
202+
rpc_mode: RpcMode::AUTHORITY,
203+
transfer_mode: TransferMode::RELIABLE,
204+
call_local: false,
205+
channel: 1,
206+
};
207+
```
208+
209+
[#902]: https://github.com/godot-rust/gdext/pull/902
210+
211+
212+
## QoL features
213+
214+
Lots of little things have been added to make your life easier. Here are some highlights related to enums:
215+
216+
```rust
217+
// Bitmask support for known enum combinations.
218+
let shifted_key = Key::A | KeyModifierMask::SHIFT;
219+
220+
// String conversions.
221+
let b: BlendMode = BlendMode::MIX;
222+
let s: &str = b.as_str(); // "MIX"
223+
224+
// Complex ordinals.
225+
#[derive(GodotConvert)]
226+
#[godot(via = i64)]
227+
enum SomeEnum {
228+
A = (1 + 2), // Use non-literal expressions.
229+
B,
230+
C = (AnotherEnum::B as isize), // Refer to other constants.
231+
}
232+
```
233+
234+
**Panics** have become much more helpful, thanks to 0x53A adding source locations to error messages ([#926]).
235+
236+
Library usage is now more robust due to various validations:
237+
- `#[class(tool)]` is required for classes that need an editor context ([#852]).
238+
- `Array<i8>` etc. now verify that elements are in range ([#853]).
239+
- Disallow `Export` if class doesn't inherit `Node` or `Resource` ([#839]).
240+
- Disallow `Export` for `Node`s if the base class isn't also `Node` ([#841]).
241+
242+
[#839]: https://github.com/godot-rust/gdext/pull/839
243+
[#841]: https://github.com/godot-rust/gdext/pull/841
244+
[#852]: https://github.com/godot-rust/gdext/pull/852
245+
[#853]: https://github.com/godot-rust/gdext/pull/853
246+
[#926]: https://github.com/godot-rust/gdext/pull/926
247+
248+
249+
## Performance
250+
251+
Interactions with Godot have been boosted quite a bit, in particular:
252+
253+
- Pass-by-ref alleviating ref-counting operations which use thread synchronization.
254+
- Cached internal object pointers ([#831]), no longer fetching it through Godot's object database.
255+
- Complete rewrite of `ClassName`, using global backing memory with interned class strings ([#834]).
256+
- Removed panic hooks in Release mode ([#889]).
257+
258+
Many thanks to Ughuu for driving multiple improvements and providing detailed benchmarks.
259+
260+
[#831]: https://github.com/godot-rust/gdext/pull/831
261+
[#834]: https://github.com/godot-rust/gdext/pull/834
262+
[#889]: https://github.com/godot-rust/gdext/pull/889
263+
264+
265+
## Conclusion
266+
267+
This release is a big step forward when it comes to UX and interacting with Godot APIs. At the same time, v0.2 also lays the groundwork for many future additions.
268+
269+
A ton of features, bugfixes and tooling enhancements haven't been covered in this post. This time, our [changelog] was so big that subsections were necessary to keep an overview. Check it out!
270+
271+
If you have existing 0.1 code and feel overwhelmed, there is a [migration guide][migrate-v0.2] to help you out.
272+
273+
274+
[api-onready]: https://godot-rust.github.io/docs/gdext/master/godot/obj/struct.OnReady.html
275+
[changelog]: https://github.com/godot-rust/gdext/blob/master/Changelog.md#v020
276+
[gdscript-rpc]: https://docs.godotengine.org/en/stable/tutorials/networking/high_level_multiplayer.html#remote-procedure-calls
277+
[migrate-v0.2]: https://godot-rust.github.io/book/migrate/v0.2.html
Loading

0 commit comments

Comments
 (0)