|
| 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 | + |
| 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 |
0 commit comments