🤖

Godot EngineからRustを呼ぶ

2020/12/04に公開

はじめに

この記事は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

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

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

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

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

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

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

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

Step4.ライブラリを呼ぶ

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

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

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

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

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

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

その後、先ほど保存した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番目かと言うと、程よく重かったから ↩︎

Discussion