Godot EngineからRustを呼ぶ

公開:2020/12/04
更新:2020/12/05
9 min読了の目安(約8500字TECH技術記事

はじめに

この記事はRust 3 Advent Calendar 2020の6日目の記事です。前回はbanatechさんが
actix, teraについて
書いてくださったようです。

この記事はオープンソースのゲームエンジンである、Godot EngineからRustでビルドした動的ライブラリを呼び出す方法を日本語でまとめ、簡単にベンチマークを取ったものになります。

また、この記事は Godot Rust を参考にして書かれており、筆者が実践の中で躓いたことを追記しています。

Godot Engineとは

Godot Engineはオープンソースかつ比較的軽量に動作するゲームエンジンで、以下のような特徴があります。

  • 無料で使える
  • ロイヤリティフリー
  • レンダラーが比較的優秀
  • エディターが親切

2Dツールが比較的豊富なのも、個人的には良いと思える点です。

  • タイルマップ
  • 光源と陰影処理
  • メッシュによる変形
  • パーティクル

Godot組み込み言語

Godotではゲームを記述するための組み込み言語として4つの言語を選ぶことができます。

  • GDScript
  • VisualScript
  • GDNative / C++
  • .NET / C#

簡単に書くときはGDScriptやVisualScript、パフォーマンスや外部のライブラリを用いたいときはGDNativeやC#で、という形でしょうか。いいですね。

今回はこのGDNativeの機構を用いてRustでビルドした動的ライブラリを呼んでみたいと思います。

Rustを呼ぶことのメリット

そんなにちゃんと考察していないのですが以下のようなことがあげられると思います。

  • クレートやビルドツールなど、Rustのエコシステムを活用することができる
  • Rustに慣れた人であれば、慣れ親しんだ文法で開発を行うことができる
  • プラットフォーム独自の言語にロックインされない
  • 安全性が高く、バグの少ないコードを書くことができる。
  • Rustはクロスコンパイルも比較的優秀なのでGodot Engineのターゲットを限定せずに済む
  • 𝓛𝓞𝓥𝓔

Godot EngineでRustからHello World

環境

  • Godot > 3.2
  • Rust > 1.41
  • Windows
  • RustはWSLのUbuntu上での環境

Step1. Rustのプロジェクトを作る

以下のコマンドでまずRustのプロジェクトを作りましょう。
作る場所はGodotのプロジェクトとは別の場所に作ると良いみたいです[1]

 cargo new --lib mygame

できたRustプロジェクトのCargo.tomlを以下のように編集しましょう。

Cargo.toml
[lib]
# Cの動的ロードライブラリとしてコンパイルする
crate-type = ["cdylib"]
[dependencies]
# godotエンジンのapiをバインディングしたクレートを使う
# 記事を書いている現在では"0.9.1"が最新
gdnative = "0.9.1" 

Step2. ライブラリをビルドする

src/lib.rsを下記のように書き換えましょう。

src/lib.rs
use gdnative::prelude::*;

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

#[gdnative::methods]
impl HelloWorld {
    fn new(_owner: &Node) -> Self {
        HelloWorld
    }

    #[export]
    fn _ready(&self, _owner: &Node) {
        godot_print!("hello, world.")
    }
}

fn init(handle: InitHandle) {
    handle.add_class::<HelloWorld>();
}

godot_init!(init);

その後、以下のコマンドでビルドを実行しましょう。

cargo build

ビルドの際、環境によっては以下のようなエラーが出て、clangのライブラリが必要になることがあるかもしれません。

'Unable to find libclang: "couldn't find any valid shared libraries matching: ['libclang.so', 'libclang-.so', 'libclang.so.', 'libclang-.so.'], set the LIBCLANG_PATH environment variable to a path where one of these files can be found (invalid: [])"'

筆者はWSL環境でビルドを行ったので、ubuntuにclangを導入することで解決できました。
その際は

sudo apt update && sudo apt install clang 

でClangを導入できます。

ビルドが成功すると、target/debug というフォルダにlibmygame.dllというファイルができているかと思います。これをGodot Engineのプロジェクトフォルダにコピーします。

筆者はWSL環境でビルドしたので.soファイルが作られましたが、Macではdylib、Windowsでは.dllが作られます。
.soファイルはwindows環境のGodot Engineで実行することができないので、こちらを参考に、Windows環境へのクロスコンパイルを行いました。

WSL(ubuntu)のシェル
# 64bitのWindows環境のツールチェインを追加
rustup toolchain install stable-x86_64-pc-windows-gnu
# 64bitのWindows環境をビルドターゲットに追加
rustup target add x86_64-pc-windows-gnu
# mingwを追加
sudo apt-get install -y mingw-w64

下記のようにリンカーの情報を追加

~/.cargo/config
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"

その後ターゲットを指定してビルド

cargo build --target x86_64-pc-windows-gnu

その後、target/x86_64-pc-windows-gnu/debug/にあるmygame.dllをGodotのプロジェクトにコピーしましょう。

Step3.Godotのプロジェクトでの設定

Godotのプロジェクトフォルダに、ビルドしたmygame.dllを配置します。
その後インスペクタから新規作成をして、GDNativeLibraryを追加します。
新規作成
新規作成

GDNativeLibraryのプラットフォームの指定がエディタ中央にでますので、対応するプラットフォームのライブラリを指定します。下記の画像の場合はWindowsの64bit環境です。

dllファイルを設定
dllファイルを設定

ライブラリを指定したら、インスペクタのセーブボタンから、GDNativeLibraryを保存してください。その際、拡張子が.gdnlibになっていることを確認してください。デフォルトでは.tresになっています。

ここではnew_gdnativelibrary.gdnlibというファイル名でgodotのプロジェクトフォルダに保存しました。

[Ctrl+S]コマンドなどでシーンを保存するだけではこのGDNativeLibraryは保存されません。

Step4.ライブラリを呼ぶ

先ほど保存したGDNativeLibraryを使って、Hello_World 構造体を作ることができます。

まずNodeノードを用意しましょう。

step.2でのlib.rsをコピペしたのであれば、継承元の型であるNode
#[inherit(gdnative::Node)]の中でも使われていることに気が付くかと思います。

lib.rs
//前略
#[derive(gdnative::NativeClass)]
#[inherit(gdnative::Node)]
struct HelloWorld;
//後略

もしSpatialSpriteといった他のタイプのノードを継承元として使いたい場合はNodeを書き換えてください。

そしてスクリプトをアタッチしてください。その際、以下のことに注意してください。

  1. 言語をGDScriptからNativeScriptに変えること
  2. クラス名をHelloWorldと入力すること

クラス名はlib.rsで指定したクラス名にするべきです。そうでなければmodules/gdnative/nativescript/nativescript.cpp:92 - Condition "!script_data" is true.というエラーが発生します。

スクリプトをアタッチ
スクリプトをアタッチ

ライブラリをロード
ライブラリをロード

その後、先ほど保存したnew_gdnativelibrary.gdnlibを読みます。

そして実行ボタンを押すことで、コンソールにhello, worldの文字列が表示されるかと思います。

hello, world
コンソールにhello, worldが表示された。

無事にRustで書いたライブラリをロードすることに成功しましたね。

Step5.補足

GDNativeのドキュメント(cargo doc --openをプロジェクトフォルダで実行してください)とGodotのドキュメントを一緒に見ることで理解が深まると思います。例えば、呼び出しシグネチャをRustでのドキュメントで、デフォルト値をGodotのドキュメントで知ることができます。

もしスタックトレースをリリースビルドでも見たい場合はCargo.toml ファイルにこのように書いてください。

Cargo.toml
[profile.release]
debug = True

応用実験

以上までがGodot Rustをなぞった簡単な導入です。
この記事ではせっかくなのでRustで何か書いてみましょう。

Rustでフィボナッチ数列を計算する

フィボナッチ数列をRustで実行して、雑ですが時間を計ってみましょうか。
以下のようなコードをビルドしてgodotに読み込ませてみました。

フィボナッチ数列の45番目[2]を再帰的に計算させ、その時間を計るようなコードです。
時間を計測するのにchronoクレートを用いています。

lib.rs
use gdnative::prelude::*;
use chrono::{Local, Duration};

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

#[gdnative::methods]
impl HelloWorld {
    fn new(_owner: &Node) -> Self {
        HelloWorld
    }

    #[export]
    fn _ready(&self, _owner: &Node) {
        let start_time = Local::now();
        godot_print!("Start calculation Fibonacci from cdylib: ");
        let v = fibonacci(45);
        let end_time = Local::now();
        let duration: Duration = end_time - start_time;
        let end = format!("{}{}{}{}","Value is ", v ," Passed Time: ", duration.num_seconds());
        godot_print!("{}", end);
    }
}

fn init(handle: InitHandle) {
    handle.add_class::<HelloWorld>();
}

fn fibonacci(n: u64) -> u64 {
    return match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n-1) + fibonacci(n-2)
    }
}

godot_init!(init);

これを実行してみると、以下のような結果になりました。

Rustでフィボナッチ数列を計算
Rustでフィボナッチ数列の45番目を計算すると12秒かかりました。

GDScriptでフィボナッチ数列を計算する

これをGDScriptでやったらどうなるでしょうか? 以下のようなコードを実行してみました。

node.gd
extends Node

func _ready():
    var start_sec = OS.get_unix_time()
    print("start calculation")
    print(fibonacci(45))
    var end_sec = OS.get_unix_time()
    print("passed time is " + String(end_sec-start_sec))

func fibonacci(n):
    match n:
        0:
            return 0
        1:
            return 1
        _:
            return fibonacci(n-1) + fibonacci(n-2)

以下のような結果になりました。

GDScriptでフィボナッチ数列を計算
GDScriptでフィボナッチ数列の45番目を計算すると1775秒=29分35秒かかりました。

フィボナッチの結果

時間の計測方法にノイズが入っている可能性はありますが、以下のような結果となりました。

  • Rustを使った場合: 12秒
  • GDScriptを使った場合:1775秒

自分でも思っていた以上にRustが早くてびっくりしています。
GDScriptの結果を待っている間、暇過ぎてシャワーを浴びることに成功しました。

これだけ速度に差があるのであれば重たい処理をRustに委譲することは有効的な場面がでてくるかもしれませんね。

終わりに

今回の記事ではGodot EnginからRustのライブラリを呼び出す方法を紹介し、その後応用例としてフィボナッチ数列の計算を比較してみました。

フィボナッチ数列の計算結果から、比較的重たいアルゴリズムで、ゲームエンジンのAPIに依存しないように書けるようなゲームロジックがあれば、Rustの出番はでてくるのではないかなと思いました。

どんでん返しにはなりますが、Godot EngineはC#を正式にサポートしているので、どうしてもというケース以外はC#を使った方がよいと思います。

この記事は初心者が調べながら書いたもので、間違いやわかりにくい所がたくさん含まれていると思います。もしよろしければご指摘くださいませ。

余談:試してみたいこと

この記事を書いている途中に思いついたけれど調べて試す気力のないものをいかに列挙します。気が向いたら追記しますので僕のツイッターをフォローしてください。

  • Rustのビルドの度にライブラリをコピペしないといけないのは面倒なのでなんとかシンボリックリンクなどでできないか
  • C#でのフィボナッチ数列の計算
  • nannouなどのクリエイティブコーディングの環境を呼び出すことができれば、インタラクティブオーディオやarduinoなどとの連携が行いやすくなるのではないか
  • Godot Engineはnintendo switchなどのコンソール機にも対応しているけれど、Rustはコンソール機向けのクロスコンパイルは可能なのか
  • フィボナッチはゲームエンジン的には必要のない処理なので、ダイクストラ法とかAstarとかだとどうなるのか
  • Godot EngineのほかのノードからGDNativeの関数を呼べるのか
脚注
  1. Godotはプロジェクト内のファイルを監視しており、常にRustプロジェクトのファイルをインポートしようとするため、プログレスバーが進まなくなるそうです。 ↩︎

  2. なぜ45番目かと言うと、程よく重かったから ↩︎