Godot EngineからRustを呼ぶ
はじめに
この記事は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を以下のように編集しましょう。
[lib]
# Cの動的ロードライブラリとしてコンパイルする
crate-type = ["cdylib"]
[dependencies]
# godotエンジンのapiをバインディングしたクレートを使う
# 記事を書いている現在では"0.9.1"が最新
gdnative = "0.9.1"
Step2. ライブラリをビルドする
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ファイルを設定
ライブラリを指定したら、インスペクタのセーブボタンから、GDNativeLibrary
を保存してください。その際、拡張子が.gdnlib
になっていることを確認してください。デフォルトでは.tres
になっています。
ここではnew_gdnativelibrary.gdnlib
というファイル名でgodotのプロジェクトフォルダに保存しました。
Step4.ライブラリを呼ぶ
先ほど保存したGDNativeLibrary
を使って、Hello_World
構造体を作ることができます。
まずNode
ノードを用意しましょう。
そしてスクリプトをアタッチしてください。その際、以下のことに注意してください。
- 言語を
GDScript
からNativeScript
に変えること - クラス名を
HelloWorld
と入力すること
スクリプトをアタッチ
ライブラリをロード
その後、先ほど保存したnew_gdnativelibrary.gdnlib
を読みます。
そして実行ボタンを押すことで、コンソールにhello, world
の文字列が表示されるかと思います。
コンソールにhello, worldが表示された。
無事にRustで書いたライブラリをロードすることに成功しましたね。
Step5.補足
GDNativeのドキュメント(cargo doc --open
をプロジェクトフォルダで実行してください)とGodotのドキュメントを一緒に見ることで理解が深まると思います。例えば、呼び出しシグネチャをRustでのドキュメントで、デフォルト値をGodotのドキュメントで知ることができます。
もしスタックトレースをリリースビルドでも見たい場合はCargo.toml
ファイルにこのように書いてください。
[profile.release]
debug = True
応用実験
以上までがGodot Rustをなぞった簡単な導入です。
この記事ではせっかくなのでRustで何か書いてみましょう。
Rustでフィボナッチ数列を計算する
フィボナッチ数列をRustで実行して、雑ですが時間を計ってみましょうか。
以下のようなコードをビルドしてgodotに読み込ませてみました。
フィボナッチ数列の45番目[2]を再帰的に計算させ、その時間を計るようなコードです。
時間を計測するのにchrono
クレートを用いています。
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でフィボナッチ数列の45番目を計算すると12秒かかりました。
GDScriptでフィボナッチ数列を計算する
これをGDScriptでやったらどうなるでしょうか? 以下のようなコードを実行してみました。
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でフィボナッチ数列の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の関数を呼べるのか
Discussion