Godot 3.1 and Rust

Godot supports native scripts (announcement and architecture) and recently added official support for C# through GDNative. All of that work culminated in the 3.0 release of Godot early last year.

This is super exciting, and I’d love to dive right into how to get up and running with Rust, but before all that, let’s take a step back.

What is Godot

Godot is an open source game engine for 2D and 3D games. It has been getting some press recently with the news that Battle for Wesnoth is being ported to it.

Godot primarily uses its own scripting engine called GDScript, and many game developers were hesitant to switch to it since the other major game engines (Unity and Unreal Engine) use C# and C++ respectively. You can certainly use C++ in Godot, but most examples push using GDScript, and the documentation for C++ is rather poor. GDScript also isn’t very fast, so it’s a poor choice for hot code paths, like terrain generation or complex AI calculations.

To solve all this, the Godot developers decided to offer a flexible model called GDNative where any C-compatible library can be used with the engine. This is super exciting, and as far as I’m aware, is a novel approach for a serious game engine.

What is GDNative

GDNative is Godot’s solution to supporting C-compatible languages. This means you can write code in C++, Rust, or any language that follows standard C calling conventions, and most serious languages support C calling conventions.

Loading a C-compatible module isn’t very difficult. In fact, many games using existing game engines already do just this, e.g. for embedding a scripting language like Lua directly into the game.

So what’s so special about GDNative? With GDNative, Godot is aware of the library, so you can have communication both ways. This means your library can call other scripts in the engine through a standard interface, so your library looks the same as any other piece of code as far as the engine is concerned. This makes it super easy to rewrite slow parts of your library without complicating the architecture of your game.

Why Rust?

Rust has been getting attention because of its memory safety features and high level of performance. Rust refuses to compile if it can detect memory safety violations, like use after free or double free. It doesn’t guarantee that code will run without bugs, but it significantly reduces the types of bugs a given program can have, and there’s virtually no performance overhead.

One major complaint about Rust is that it’s a bit slower to write code because it enforces a certain style to make those memory safety guarantees. In game development, getting something functional quickly is often more important than making sure it’s perfect, and game developers are willing to give up a lot of performance in the early stages of development. However, when it comes time to release, they need every ounce of performance they can get out of the hot code paths.

And that’s why I find Rust so interesting. I can prototype my project in a high level language like GDScript, and then optimize slow parts in something like C++ or Rust. C++ has the library support for games, so it used to be that it was your only practical option. But now that Godot treats all code essentially the same, I get to choose between Rust and C++ without giving up features.

That’s the theory anyway. The current reality is that the Rust bindings are incomplete, but they’re good enough for many important use cases, so let’s get started.

Getting started (Rust)

First, make sure you have a Rust development environment (see here for instructions). This can all be done with stable Rust, and the version I used here is 1.34 with Rust 2018 edition, which will be the default if you’re installing Rust for the first time.

The library we will be using is called godot-rust, and it’s available on crates.io as gdnative. I found that the package has compile errors, but compiling from the latest code on master seems to work, so that is what my tutorial will be doing. The bindings are very much in development, so it’s probably best to use the latest development head for now. That’s the main difference here from the instructions on the project page.

Now, set up a new project as a library:

$ cargo init --lib

Edit the Cargo.toml to include gdnative as a dependency:

[dependencies]
gdnative = { git = "https://github.com/GodotNativeTools/godot-rust" }

And configure it to compile as a C-compatible library:

[lib]
crate-type = ["cdylib"]

And modify src/lib.rs to look like so (again, taken from the project page):

use gdnative::*;

/// The HelloWorld "class"

#[derive(NativeClass)] #[inherit(Node)] pub struct HelloWorld;

// __One__ `impl` block can have the `#[methods]` attribute, which will generate
// code to automatically bind any exported methods to Godot.

#[methods] impl HelloWorld {

    /// The "constructor" of the class.
    fn _init(_owner: Node) -> Self {
        HelloWorld
    }

    // In order to make a method known to Godot, the #[export] attribute has to be used.
    // In Godot script-classes do not actually inherit the parent class.
    // Instead they are"attached" to the parent object, called the "owner".
    // The owner is passed to every single exposed method.
    #[export]
    fn _ready(&self, _owner: Node) {
        // The `godot_print!` macro works like `println!` but prints to the Godot-editor
        // output tab as well.
        godot_print!("hello, world.");
    }
}

// Function that registers all exposed classes to Godot
fn init(handle: gdnative::init::InitHandle) {
    handle.add_class::<HelloWorld>();
}

// macros that create the entry-points of the dynamic library.
godot_gdnative_init!();
godot_nativescript_init!(init);
godot_gdnative_terminate!();

Now run cargo build to build the library, and your library should end up in target/debug (e.g. lib<project_name>.so if you’re on Linux).

And there we go, we now have our library compiled and ready to use!

Getting started (Godot)

Now that our Rust library is compiled and ready to go, it’s time to plug it into Godot. I’m using Godot 3.1, which is the latest stable release of Godot as of this writing.

To use a GDNative library, we need to make a GDNativeLibrary resource. To do this, go to the “Inspector” panel in the Godot editor by clicking “new resource” in the top left. Fill in the path to the library for your platform, and click the “Save” button in the inspector panel (this is super important, since resources don’t automatically save).

Next, select a node and click the “Attach Script” button in the context menu for the node, the “add script” button in the “Scene” panel, or “New Script” under the “Script” section of the “Inspector” panel. Select “Native Script” and set the class to the name of your struct in your Rust code (HelloWorld in our case) and give it some new path. Note: this path is not the path to the resource we just created, it’s just some container that we can attach our library to.

Finally, now that we have our native script resource and our node has a script instance, we can combine the two. With the node selected, go to the “Inspector” panel under “Script” (it’ll be under the base Node type), click the drop-down menu and select “Load”, and then locate your script resource that we created in the first step.

Save everything and run your project. If everything worked correctly, you should see hello, world. printed in the console. Hooray! You now have a Rust module in your Godot game! If you want to make changes, just rebuild and relaunch the game.

Now what?

Now you can replace entire scripts with Rust code. However, doing this is pretty tricky because the documentation is next to non-existant. However, the project has a couple of examples, one of which is the example we used here.

As I mentioned in the beginning of this post, I’m mostly excited to rewrite parts of my code in Rust seamlessly, and having to rewrite entire scripts is not seamless.

So, let’s look at how to create and call arbitrary functions.

Call functions from GDScript

Let’s say we wanted to optimize a factorial function. Here’s the slow implementation in GDScript:

func factorial(n):
    var result = 1;
    while n > 0:
        result = result * n
        n = n - 1
    return result

And our version in Rust:

#[export]
fn factorial(&self, _owner: gdnative::Node, n: u32) -> u64 {
    let mut n = n as u64;
    let mut result = 1;
    while n > 0 {
        result = result * n;
        n = n - 1;
    }
    result
}

Note the differences with the _ready function above, we basically just added an argument n and a return value of type u64. The #[export] above the function tells the compiler that we intend to export this to Godot.

So, now let’s call it from our GDScript file. First, we’ll need to load the script like we do any other resource:

onready var hello = preload("res://helloworld.gdns").new()

Then call it somewhere:

hello.factorial(7)

Let’s compare the timing differences between Rust and GDScript to see if we made an improvement (we’ll run it 100k times, because factorial is fast):

var num = 10
print('Computing factorial for: ', num)

var gd_start = OS.get_ticks_msec()
var gd_res = factorial(num)
for _n in 100000:
    factorial(num)
var gd_total_time = OS.get_ticks_msec() - gd_start
print('GDScript: ', gd_res, " in ", gd_total_time, " milliseconds")

var rust_start = OS.get_ticks_msec()
var rust_res = hello.factorial(num)
for _n in 100000:
    hello.factorial(num)
var rust_total_time = OS.get_ticks_msec() - rust_start
print('Rust: ', rust_res, " in ", rust_total_time, " milliseconds")

if gd_res != rust_res:
    print("they don't match!!")
print("Rust was ", gd_total_time - rust_total_time, " milliseconds faster")

And here are my results:

Computing factorial for: 10
GDScript: 3628800 in 232 milliseconds
Rust: 3628800 in 88 milliseconds
Rust was 144 milliseconds faster

So rust was about 2.5x faster than GDScript!

But wait, we were doing that in debug mode, what about building for release?

Let’s try it out! First, compile in release mode:

$ cargo build --release

And then go back to our resource (from the first step, not the script object we created) and change the path to the library from target/debug to target/release. Here’s what I got:

Computing factorial for: 10
GDScript: 3628800 in 231 milliseconds
Rust: 3628800 in 38 milliseconds
Rust was 193 milliseconds faster

Which means that Rust is more than 2x faster in release mode, bringing the difference to more than 6 times faster than GDScript!

Takeaway

So, rewriting GDScript in Rust is a very reasonable option for optimizing a critical path in your game. I don’t recommend doing everything in Rust because GDScript is just so much more productive, but it’s nice to know that even Rust in debug mode is significantly faster than GDScript, so you can still have a reasonably fast edit/reload cycle when porting code to Rust for performance.

We’ve only covered the basics here, and there is plenty that isn’t supported yet, but it’s usable today and I’ve even seen reports that it should work for cross-compiling to other projects, provided you have the cross compiler installed for Rust.

I’m excited for the future of Godot and Rust, and I hope this post has been helpful in getting you started. I may write some posts in the future that get more in depth, so stay tuned!

Tags


Recent posts

Godot 3.1 and Rust

Go 1.11 and vgo

Getting Started With Hugo


Archives

2019 (1)
2018 (2)