Lokkij's game development thoughts

Three Godot+Rust tricks

I've been prototyping a turn-based game for a couple months now. I've landed on the Godot engine with the godot Rust crate1 as the way to go for that prototype. Here's three2 nice tricks that I figured out over the past couple months that might help you out.

If any of these have been helpful to you, or if you have any cool tricks of your own - let me know on Bluesky or by email, I would love to hear your thoughts!

Logging to the Godot console

By default, printing to stdout (via println! or the log crate) doesn't actually make your output show up in the Godot console. godot provides the godot_print! macro, which does show up in the Godot console and is a drop-in replacement for println!. Convenient!

But I like my logging to have colors and timestamps, and one of my dependencies sometimes emits useful logs. So here's a quick implementation of a logger (for use with the log crate) that prints its logs to the Godot console:

pub struct GodotLogger;

impl Log for GodotLogger {
    fn enabled(&self, _: &log::Metadata) -> bool {
        true
    }

    fn log(&self, record: &log::Record) {
        if self.enabled(record.metadata()) {
            let level_str = match record.level() {
                Level::Error => "[color=dc143c][b]ERROR[/b][/color]",
                Level::Warn => " [color=ff8c00][b]WARN[/b][/color]",
                Level::Info => " [color=#7fffd4]INFO[/color]",
                Level::Debug => "[color=#f5f5dc]DEBUG[/color]",
                Level::Trace => "[color=#808080]TRACE[/color]",
            };

            let target = record.target();
            let mut message = record.args().to_string();
            match record.level() {
                Level::Debug | Level::Trace => {
                    message = format!("[color=#aaaaaa]{message}[/color]")
                }
                _ => { /* Nothing to do here */ }
            }
            let timestamp = Utc::now().format("%H:%M:%S");

            godot_print_rich!(
                "[color=#808080]{timestamp}[/color] {level_str} [color=#808080]{target}:[/color] {message}"
            );
        }
    }

    fn flush(&self) {
        // Nothing to do here
    }
}

This allows you to use the trusty log crate to do your logging - and if any of your dependencies emit logs, those will get captured too. Just make sure to initialize it in your extension's entry point:

#[gdextension]
unsafe impl ExtensionLibrary for DefianceExtension {
    fn on_stage_init(stage: InitStage) {
        if stage == InitStage::MainLoop {
            log::set_logger(&GodotLogger)
                .map(|()| log::set_max_level(LevelFilter::Trace))
                .expect("Failed to install logger");
        }
    }
}

Preventing accidental runs of old code3

Fast iteration is vital in game development. To this end, I use watchexec to watch my Rust files and recompile automatically when they change:

watchexec -e rs,toml 'cargo build'

This means I can make my changes, tab over to Godot, and immediately test my new changes. There's one small annoyance though: sometimes, compiling takes a few seconds. This means that if you press play too fast, you might be unknowingly playing the old, unchanged version. This used to happen to me quite frequently.

As a bandaid solution, I now read the file metadata on the compiled dynamic library when the game starts and log its age to the Godot console:

#[gdextension]
unsafe impl ExtensionLibrary for DefianceExtension {
    fn on_stage_init(stage: InitStage) {
        if stage == InitStage::MainLoop {
            // ... set up the logger, etc. ...

            if !Engine::singleton().is_editor_hint() {
                let metadata = std::fs::metadata("../target/debug/libmygame.dylib")
                    .expect("Failed to retrieve metadata for dynamic library");
                let delta = SystemTime::now()
                    .duration_since(metadata.created().unwrap())
                    .unwrap()
                    .as_secs();
                
                log::info!("Game initialized, build from {delta} seconds ago");
                if delta > 60 {
                    log::info!("This build is over a minute old");
                }
            }
        }
    }
}

This way, it's easy to tell when you're playing an older build than you expected. If you set up easy keyboard shortcuts Godot for running and stopping your project, it's also super quick to rerun with the new code - by the time you notice, your new dynamic library is usually done compiling.

Verifying that loaded files exist at compile time

Most likely, you're loading a bunch of stuff from the resource folder in your code. I've written this small macro that can verify at compile time that the file you're loading actually exists:

let scene: Gd<PackedScene> = static_load!("scenes/ActorDisplay.tscn");

If res://scenes/ActorDisplay.tscn doesn't exist, you get a nice compile-time error telling you it can't find the file.

This is the definition:

#[macro_export]
macro_rules! static_load {
    ($path:literal) => {{
        if cfg!(debug_assertions) {
            const {
                include_bytes!(
                    concat!(env!("CARGO_MANIFEST_DIR"), "/../mygame-godot/", $path)
                ).len()
            };
        }
        ::godot::prelude::load(concat!("res://", $path))
    }};
}

It uses include_bytes! to make the compiler look up the file and verify it exists, but don't worry - because we immediately throw away the result, the file won't actually be included in your binary. Just make sure to adjust the path in the macro so it points to your Godot resource root. And of course, this only works for statically known paths.

Wrapping up

I've really been enjoying my time with Rust+Godot - frankly much more than I was expecting. I have a few gripes with Godot, but it's been an overall good experience. It also helps that the godot crate is absolutely fantastic.

If you decide to try it yourself, or even if you've been developing in Godot+Rust for years: I hope at least some these tricks have are helpful!

  1. I should note that the godot crate is extremely impressive. It offers a very high level of integration with Godot, and in some areas even better validation than GDScript can offer. It rocks.

  2. A previous version of this post featured four Godot+Rust tricks, but it turned out that the fourth trick wasn't actually very useful - the same effect could be achieved trivially without the trick. Oops!

  3. An update on this: apparently someone has made a Godot plugin that you can install that automatically builds your extension for you when you press play. That's probably a better solution than mine!