🕹️ Godot Rust #
Following on from the recent, trying Godot 4 post, here, we look at Godot Rust gdext. In that previous post, I mentioned how Godot not only lets you code in officially supported GDScript, but also lets you create dynamic libraries for your Godot games written in Rust, Swift, and Zig among other languages.
In this post, I run through some resources for getting started with Godot Rust gdext and also highlight some tips that I benefited from.
🧑🏽🎓 gdext Learning Resources #
gdext
provides GDExtension bindings for Godot 4. If you are working
with Godot 4, skip over gdnative
resources, which relate to the older
Godot 3 API.
The best starting point is probably the godot-rust book , which starts by setting up with gdext and runs through a basic code example. The book then move on to more advanced topics, these are really helpful, and you will probably want to keep the book open even if you jump to working on your own game after working through the initial chapters.
The official, online Rust godot docs document APIs. If you have to work offline, cargo lets you open these from a local source in a browser:
cargo doc --open --package godot
Since the gdext APIs mirror GDScript APIs, you will often want to check the GDScript API docs for additional background.
Another fantastic resource is the example game code in the gdext GitHub repo for a Dodge the Creeps game (which will sound familiar if you have followed the official Godot, GDScript-based, tutorial ). You can try building it or just dip into for help to unblock if you get stuck working on your own game.
🧱 What I Built #
After working though the gdext Hello World, I thought a good way to learn more APIs would be to start converting a game I already had from GDScript to Godot Rust gdext. For this, I picked the Knights to See You game I made by following the How to make a Video Game - Godot Beginner Tutorial .
I followed the guidelines in the godot-rust book, but made a small change, in the project file structure. This made it slightly more convenient to work in the Terminal from the project root folder, and also simplified the project CI configuration.
![Godot Rust gdext: a 2D platform style game view with a blue background in three bands, becoming deeper blue as you descend. The foreground features brown and grey stone platforms. The player character has a knight sprite, and you can see coins, a bottle, a tree, and a rope bridge. Text to the left and centre of the view reads "Space to jump".](https://rodneylab-nebula-a.shuttleapp.rs/v1/lab/post/godot-rust-gdext/godot-rust-gdext-knights-to-see-you-game.4172480b6fb1.png?w=592&h=333&fit=min&auto=format)
📂 Folder Structure #
The change I made from the godot-rust book, which I alluded to before was to set the project up
using Cargo Workspaces . The rust
folder is exactly as the book recommends, and contains
a Cargo.toml
file.
├── Cargo.toml├── godot│ ├── assets│ │ ├── fonts│ │ ├── music│ │ ├── sounds│ │ └── sprites│ ├── export_presets.cfg│ ├── knights-to-see-you.gdextension│ ├── project.godot│ ├── scenes│ └── scripts└── rust├── Cargo.toml└── src├── lib.rs└── player.rs
I, just, added another Cargo.toml
file to the root directory, where
I created a new workspace, adding that rust
directory:
[workspace]members = ["rust"]resolver = "2"
This changes the Rust output target
directory, moving it up a level,
so I had to update the paths listed in my godot/<PROJECT>.gdextension
file:
1 [configuration]2 entry_symbol = "gdext_rust_init"3 compatibility_minimum = 4.24 reloadable = true56 [libraries]7 linux.debug.x86_64 = "res://../target/debug/librust.so"8 linux.release.x86_64 = "res://../target/release/librust.so"9 windows.debug.x86_64 = "res://../target/debug/librust.dll"10 windows.release.x86_64 = "res://../target/release/librust.dll"11 macos.debug = "res://../target/debug/librust.dylib"12 macos.release = "res://../target/release/librust.dylib"13 macos.debeg.arm64 = "res://../target/debug/librust.dylib"14 macos.release.arm64 = "res://../target/release/librust.dylib"
👀 Rust Hot Reloading and Watching Files #
GDExtension for Godot 4.2 supports hot reloading out of the box. To speed up your coding feedback cycle, use cargo watch to recompile your Rust code automatically when you save a Rust source file. Install cargo watch (if you need to):
cargo install cargo-watch --locked
Then, you can run:
cargo watch -cx check -x clippy -x build
which will clear the window, run cargo check
, cargo clippy
then, cargo build
each time you save the Rust source. Now you can
hit save then jump straight to Godot Engine to test play the game.
🦀 gdext API Snippets #
I started the switch from GDScript to Godot Rust gdext with the Player scene. In the original
game, the Player Scene was a CharacterBody2D
.
Rust does not easily handle Object-oriented Programming inheritance, and gdext opts for a
composition approach. Following the book, you will see the recommended pattern, here, is to create
a Player
struct in Rust with a base
field of type Base<ICharacterBody2D>
. That base
field then provides access to the character properties that you are familiar with from GDScript.
Here are some API snippets (with equivalent GDScript) to give you a feel for gdext
.
Updating Character Velocity #
Using GDScript:
velocity.x = 100.0velocity.y = 0.0
Using gdext
:
self.base_mut().set_velocity(Vector2::new(100.0, 0.0));
base_mut()
, here, returns a mutable reference to the base
field on Player
, mentioned above. You set and get CharacterBody2D
properties using self.base()
and self.base_mut()
.
Gravity and Project Settings #
To get project default gravity value with GDScript, you can use:
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
The equivalent in gdext is:
let gravity = ProjectSettings::singleton().get_setting("physics/2d/default_gravity".into()).try_to::<f64>().unwrap();
Setting Animations #
The Player scene has an AnimatedSprite2D
node. Typically, you can reference
that node in GDScript with something like this:
@onready var animated_sprite = $AnimatedSprite2D# TRUNCATED...animated_sprite.play("idle")
In gdext
, you can set the sprite like so:
let mut animated_sprite = self.base().get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");// Play animationanimated_sprite.play_ex().name("idle".into()).done();
Full Comparison #
For reference, here is the full code for the Rust Player
class (rust/src/player.rs
):
rust/src/player.rs
— click to expand code.
1 use godot::{2 builtin::Vector2,3 classes::{AnimatedSprite2D, CharacterBody2D, ICharacterBody2D, Input, ProjectSettings},4 global::{godot_print, move_toward},5 obj::{Base, WithBaseField},6 prelude::{godot_api, GodotClass},7 };89 #[derive(GodotClass)]10 #[class(base=CharacterBody2D)]11 struct Player {12 speed: f64,13 jump_velocity: f64,1415 base: Base<CharacterBody2D>,16 }1718 enum MovementDirection {19 Left,20 Neutral,21 Right,22 }2324 #[godot_api]25 impl ICharacterBody2D for Player {26 fn init(base: Base<CharacterBody2D>) -> Self {27 godot_print!("Initialise player Rust class");28 Self {29 speed: 130.0,30 jump_velocity: -300.0,3132 base,33 }34 }3536 fn physics_process(&mut self, delta: f64) {37 let Vector2 {38 x: velocity_x,39 y: velocity_y,40 } = self.base().get_velocity();4142 let input = Input::singleton();4344 // handle jump and gravity45 let new_velocity_y = if self.base().is_on_floor() {46 if input.is_action_pressed("jump".into()) {47 #[allow(clippy::cast_possible_truncation)]48 {49 self.jump_velocity as f3250 }51 } else {52 velocity_y53 }54 } else {55 let gravity = ProjectSettings::singleton()56 .get_setting("physics/2d/default_gravity".into())57 .try_to::<f64>()58 .expect("Should be able to represent default gravity as a 32-bit float");59 #[allow(clippy::cast_possible_truncation)]60 {61 velocity_y + (gravity * delta) as f3262 }63 };6465 // Get input direction66 let direction = input.get_axis("move_left".into(), "move_right".into());67 let movement_direction = match direction {68 val if val < -f32::EPSILON => MovementDirection::Left,69 val if (-f32::EPSILON..f32::EPSILON).contains(&val) => MovementDirection::Neutral,70 val if val >= f32::EPSILON => MovementDirection::Right,71 _ => unreachable!(),72 };7374 let mut animated_sprite = self75 .base()76 .get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");7778 // Flip the sprite to match movement direction79 match movement_direction {80 MovementDirection::Left => animated_sprite.set_flip_h(true),81 MovementDirection::Neutral => {}82 MovementDirection::Right => animated_sprite.set_flip_h(false),83 }8485 // Play animation86 let animation = if self.base().is_on_floor() {87 match movement_direction {88 MovementDirection::Neutral => "idle",89 MovementDirection::Left | MovementDirection::Right => "run",90 }91 } else {92 "jump"93 };94 animated_sprite.play_ex().name(animation.into()).done();9596 // Apply movement97 #[allow(clippy::cast_possible_truncation)]98 let new_velocity_x = match movement_direction {99 MovementDirection::Neutral => {100 move_toward(f64::from(velocity_x), 0.0, self.speed) as f32101 }102 MovementDirection::Left | MovementDirection::Right => direction * (self.speed) as f32,103 };104105 self.base_mut().set_velocity(Vector2 {106 x: new_velocity_x,107 y: new_velocity_y,108 });109110 self.base_mut().move_and_slide();111 }112 }
See the link further down for the full project code.
Again, for reference, here is the previous GDScript code for the Player:
godot/scripts/player.gd (deleted)
— click to expand code.
1 extends CharacterBody2D23 const SPEED := 130.04 const JUMP_VELOCITY := -300.056 # Get the gravity from the project settings to be synced with RigidBody nodes.7 var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")89 @onready var animated_sprite = $AnimatedSprite2D101112 func _physics_process(delta):13 # Add the gravity.14 if not is_on_floor():15 velocity.y += gravity * delta1617 # Handle jump.18 if Input.is_action_just_pressed("jump") and is_on_floor():19 velocity.y = JUMP_VELOCITY2021 # Get the input direction: -1, 0 or 122 var direction := Input.get_axis("move_left", "move_right")2324 # Flip the sprite25 if direction > 0:26 animated_sprite.flip_h = false27 elif direction < 0:28 animated_sprite.flip_h = true2930 # Play animation31 if is_on_floor():32 if direction == 0:33 animated_sprite.play("idle")34 else:35 animated_sprite.play("run")36 else:37 animated_sprite.play("jump")3839 # Apply movement40 if direction:41 velocity.x = direction * SPEED42 else:43 velocity.x = move_toward(velocity.x, 0, SPEED)4445 move_and_slide()
🤖 Adding Rust Library in Godot Engine #
Once you have coded and compiled the player (or other GDExtension class) to use it in your game, you just need to change the type of its Godot Scene.
Do this by right-clicking on the scene in Godot Engine (Player scene in this case) and selecting Change Type….
![Godot Rust gdext: The screen capture shows Godot Engine. A small window is in focus, in the middle of the screen, with the title "Change Type of Player". In the Search box, below the title, the user has typed "Player". In the tree view, which occupies most of the window, Player is selected, and appears as a child node of Character Body 2 D. A tooltip is also visible and reads "Player"](https://rodneylab-nebula-a.shuttleapp.rs/v1/lab/post/godot-rust-gdext/godot-rust-gdext-adding-rust-class.5f08f5aa1e2e.png?w=592&h=333&fit=min&auto=format)
Then, you need to search for the name you gave your class in the Rust code. My Rust struct was
also called Player
, and I can see it in the view as a child of CharacterBody2D
. I select this and I am done!
![Godot Rust gdext: The screen capture shows Godot Engine. In the left-hand panel, the Scene tab is active, and the user has opened the context menu for the Player Scene. The logo beside the Player label indicates that the Player is an instance of a Character Body 2 D class. In the context menu, towards the top of the bottom half of the listed options, "Change Type..." is highlighted.](https://rodneylab-nebula-a.shuttleapp.rs/v1/lab/post/godot-rust-gdext/godot-rust-gdext-finding-rust-class.f77c1e0bd2fc.png?w=592&h=333&fit=min&auto=format)
Godot Engine now links to the dynamic shared library I created in Rust.
Please enable JavaScript to watch the video 📼
🗳 Poll #
🙌🏽 Godot Rust gdext: Wrapping Up #
In this post on Godot Rust gdext, we took a look through some resources for getting started with GDExtension for Godot 4. In particular, we looked at:
- resources for getting started with GDExtension and Godot Rust;
- a tip for speeding up the coding feedback cycles; and
- how to use your GDExtension dynamic library in Godot Engine.
I hope you found this useful. As promised, you can get the full project code on the Rodney Lab GitHub repo . I would love to hear from you, if you are also new to Godot video game development. Were there other resources you found useful? Also, let me know what kind of game you are working on!
🙏🏽 Godot Rust gdext: Feedback #
If you have found this post useful, see links below for further related content on this site. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.
Just dropped a new post on setting up a Godot Rust gdext project. Covering:
— Rodney (@askRodney) July 17, 2024
— resources for getting started with GDExtension;
— adding watch to speed up feedback cycle; and
— adding your lib in Godot Engine.
Hope you find it useful!
https://t.co/Rp9HLWMefQ
Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on X (previously Twitter) and also, join the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Game Dev as well as Rust and C++ (among other topics). Also, subscribe to the newsletter to keep up-to-date with our latest projects.