Godot Rust gdext: GDExtension Rust Game Dev Bindings

Rodney Lab - Aug 7 - - Dev Community

πŸ•ΉοΈ 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
Enter fullscreen mode Exit fullscreen mode

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

πŸ“‚ 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

πŸ‘€ 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
Enter fullscreen mode Exit fullscreen mode

Then, you can run:

cargo watch -cx check -x clippy -x build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Using gdext:

  self.base_mut().set_velocity(Vector2::new(100.0, 0.0));
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

The equivalent in gdext is:

let gravity = ProjectSettings::singleton()
    .get_setting("physics/2d/default_gravity".into())
    .try_to::<f64>()
    .unwrap();
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

πŸ€– 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

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,

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .