Godot Rust gdext
GDExtension Rust Game Dev Bindings
πΉοΈ 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.
π 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:
[configuration]
entry_symbol = "gdext_rust_init"
compatibility_minimum = 4.2
reloadable = true
[libraries]
linux.debug.x86_64 = "res://../target/debug/librust.so"
linux.release.x86_64 = "res://../target/release/librust.so"
windows.debug.x86_64 = "res://../target/debug/librust.dll"
windows.release.x86_64 = "res://../target/release/librust.dll"
macos.debug = "res://../target/debug/librust.dylib"
macos.release = "res://../target/release/librust.dylib"
macos.debeg.arm64 = "res://../target/debug/librust.dylib"
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.0
velocity.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 animation
animated_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
):
use godot::{
builtin::Vector2,
classes::{AnimatedSprite2D, CharacterBody2D, ICharacterBody2D, Input, ProjectSettings},
global::{godot_print, move_toward},
obj::{Base, WithBaseField},
prelude::{godot_api, GodotClass},
};
#[derive(GodotClass)]
#[class(base=CharacterBody2D)]
struct Player {
speed: f64,
jump_velocity: f64,
base: Base<CharacterBody2D>,
}
enum MovementDirection {
Left,
Neutral,
Right,
}
#[godot_api]
impl ICharacterBody2D for Player {
fn init(base: Base<CharacterBody2D>) -> Self {
godot_print!("Initialise player Rust class");
Self {
speed: 130.0,
jump_velocity: -300.0,
base,
}
}
fn physics_process(&mut self, delta: f64) {
let Vector2 {
x: velocity_x,
y: velocity_y,
} = self.base().get_velocity();
let input = Input::singleton();
// handle jump and gravity
let new_velocity_y = if self.base().is_on_floor() {
if input.is_action_pressed("jump".into()) {
#[allow(clippy::cast_possible_truncation)]
{
self.jump_velocity as f32
}
} else {
velocity_y
}
} else {
let gravity = ProjectSettings::singleton()
.get_setting("physics/2d/default_gravity".into())
.try_to::<f64>()
.expect("Should be able to represent default gravity as a 32-bit float");
#[allow(clippy::cast_possible_truncation)]
{
velocity_y + (gravity * delta) as f32
}
};
// Get input direction
let direction = input.get_axis("move_left".into(), "move_right".into());
let movement_direction = match direction {
val if val < -f32::EPSILON => MovementDirection::Left,
val if (-f32::EPSILON..f32::EPSILON).contains(&val) => MovementDirection::Neutral,
val if val >= f32::EPSILON => MovementDirection::Right,
_ => unreachable!(),
};
let mut animated_sprite = self
.base()
.get_node_as::<AnimatedSprite2D>("AnimatedSprite2D");
// Flip the sprite to match movement direction
match movement_direction {
MovementDirection::Left => animated_sprite.set_flip_h(true),
MovementDirection::Neutral => {}
MovementDirection::Right => animated_sprite.set_flip_h(false),
}
// Play animation
let animation = if self.base().is_on_floor() {
match movement_direction {
MovementDirection::Neutral => "idle",
MovementDirection::Left | MovementDirection::Right => "run",
}
} else {
"jump"
};
animated_sprite.play_ex().name(animation.into()).done();
// Apply movement
#[allow(clippy::cast_possible_truncation)]
let new_velocity_x = match movement_direction {
MovementDirection::Neutral => {
move_toward(f64::from(velocity_x), 0.0, self.speed) as f32
}
MovementDirection::Left | MovementDirection::Right => direction * (self.speed) as f32,
};
self.base_mut().set_velocity(Vector2 {
x: new_velocity_x,
y: new_velocity_y,
});
self.base_mut().move_and_slide();
}
}
See the link further down for the full project code.
Again, for reference, here is the previous GDScript code for the Player:
extends CharacterBody2D
const SPEED := 130.0
const JUMP_VELOCITY := -300.0
# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
@onready var animated_sprite = $AnimatedSprite2D
func _physics_process(delta):
# Add the gravity.
if not is_on_floor():
velocity.y += gravity * delta
# Handle jump.
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Get the input direction: -1, 0 or 1
var direction := Input.get_axis("move_left", "move_right")
# Flip the sprite
if direction > 0:
animated_sprite.flip_h = false
elif direction < 0:
animated_sprite.flip_h = true
# Play animation
if is_on_floor():
if direction == 0:
animated_sprite.play("idle")
else:
animated_sprite.play("run")
else:
animated_sprite.play("jump")
# Apply movement
if direction:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
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β¦.
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 Engine now links to the dynamic shared library I created in Rust.
ππ½ 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.
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.