この記事は Rust Advent Calendar 2021 - Qiita 20日目の記事です!
ゲームエンジン Godot (ゴドー https://godotengine.org/ ) とRustを用いて3D1シューティングゲームを作成し、さらにソースコード等をwasm化することでWebGLゲームとして公開することができたので、その手法の解説記事となります!(環境構築がメインです)
ここから遊べます!!!!! ↓ スマホも対応! (注意:音が出ます)
- 操作は十字キーとスペースを使います
- 一度画面をクリックするとキーボードで操作できます
完成したシューティングゲームのデモ(Windows版)↓
- GitHubリポジトリ: https://github.com/anotherhollow1125/godot_wasm_rust_shooting
- Rustで書いた部分: https://github.com/anotherhollow1125/godot_wasm_rust_shooting/blob/main/shooting_rst/src/lib.rs
アピールポイント
- スマホのブラウザでも遊べる3Dゲーム! (UnityのWebGLも一応対応してるらしい...?)
- ゲームロジックの殆どがRustで書かれている! (with gdnative クレート)
はじめに
筆者の背景を一応紹介します。筆者はプログラミングとゲーム制作が趣味の大学生ですが、Unity C#のNullReferenceExceptionやその他の言語のガベコレに辟易し、Rustをメイン言語としてゲームを作れないかとここ数年模索してきました。
昨年のアドベントカレンダー記事では、Rustで3DCGを行う手法を模索しました。このほか、nannouを試したりbevyについて調べたりなどもしてきました。しかしいくらRustが好きだからと言って制作のすべてをコードベースで行うのは大変です。要はRustでもUnityみたいな開発環境がほしいわけです。
そんな折、Rustで作った動的ライブラリを利用できるGodotというゲームエンジンがあることを知りました。
このGodot、ゲームをWebGLエクスポートすることも可能とのこと!制作物はすぐに遊んでもらいたく、Webで簡単に公開できることにもこだわっていた自分としては大変好都合です。しかしRustが使えてかつWebGL化もすぐできるなんて美味しい話があるのかと思ったらそうでもなく、その場合ちょっと工夫が必要になります。
Support wasm32-unknown-emscripten target · Issue #647 · godot-rust/godot-rust
今回の記事では、このIssuesで紹介されているorion78frさんの手法を使った、Rustのソースコードをwasm化する開発環境の作成方法を前半で解説し、後半は今回作ったシューティングゲームの工夫した点などを語ります。
Godot & Rust & WebAssembly 開発環境構築編
Godot
いきなりGodotの存在だけ紹介されても困惑すると思うので、Godotについて説明します。本気で入門したい方はまずはRustのことは一旦忘れ、普通に公式のチュートリアルをやりましょう2!(Godotの環境構築に関しては割愛します、といっても本体のインストールはダウンロードしてzipを展開するだけなのでとても簡単です3。)
- Your first game — Godot Engine (stable) documentation in English
- Scenes and nodes — Godot Engine (stable) documentation in English
クリープを避けろというゲームなのですが、結構短時間で作れます。Godotの特徴を一通りつかめると思います。以下のような感じでしょうか
- ゲームオブジェクトなどはすべてNodeという単位で管理され、Nodeはツリーを形成できる
- ツリーのルートであるルートノードが1つ存在するシーンという単位でファイルとして保存される。シーンもまたノードとしたり、インスタンス化できる
- 基本的には、GDScriptというPythonライクな言語でプログラミングを行う。イベントハンドラのようなものとしてシグナルという仕組みがある
オブジェクト指向で、コンポネント指向であるようです。Unityと似た部分も多く楽に開発ができそうです!以降、Rust向け環境構築の話題になります。GodotのためのプログラムをRustで書いて、Rustによるゲーム制作を実現しましょう!
環境構築に関してのTL;DR
環境構築というのは大変です。「そんな面倒で長ったらしい環境構築しなきゃ駄目なの??ていうかここうまくいかないんですけど!本当に環境構築できるの?」ってなるのが普通です(コメント大歓迎です!)。そこで少しでも手助けするべく、今回の記事のオマケとしてDockerfileを用意しました!使い方はGitHubのREADME.mdの方を参照してください。
-
Dockerfile
: wasmへのコンパイル用に用意したDockerfileになります。 -
all_in_one_win/Dockerfile
: wasmへのコンパイルに加え、Windows向けに.dll
へもコンパイルできる環境です。VSCodeのRemote Containersとの併用を想定しています。
最初はwasmコンパイル用の環境のみ用意しようと思ったのですが、一応.dll
有りの環境も後付で用意しました。そのためこのような配置になっています。
(環境構築は遠慮しておくよという方はシューティングゲーム制作編まで飛びましょう)
Godot & Rustの開発環境整備
おそらくチュートリアルを終えた後と思われますので、次はRustのgdnativeクレートを用いて、RustでGodotゲームをプログラミングできるように環境を整えましょう!
先程挙げたGodot EngineからRustを呼ぶの記事様がとてもわかりやすかったため、これに従って環境構築します。すべてlinux向けの場合はclangを入れるぐらいで難しいことは何もないのですが、Windows & WSL2環境の場合少しだけ設定が必要です4。
依存関係導入
Rustは導入済みとします。(一応リンク: https://www.rust-lang.org/tools/install )
Rustの環境:
- cargo 1.57.0 (b2e52d7ca 2021-10-21)
まずは依存関係をインストールしていきます。本記事ではコピペ簡易化のためプロンプトの $
は省略します。
sudo apt update
sudo apt install -y build-essential clang mingw-w64
clang
と、linux環境でWindows向けにコンパイルするための mingw-w64
をインストールします。 build-essential
はRustをインストールする過程で入れているとは思われますが、一応記載しました。
そしてWindows向けのコンパイルターゲットをrustupで入れておきます。
rustup target add x86_64-pc-windows-gnu
設定とコンパイル
ここからは早速プロジェクトフォルダを作成していきましょう!
cargo new --lib hello_godot
cd hello_godot
先に面倒事を済ませましょう。 cargo
に関する設定を行うため .cargo/config.toml
ファイルを追加します。
mkdir .cargo
touch .cargo/config.toml
config.toml
の中身を以下のようにします。Windows向けにコンパイルするための設定です。
[build]
target = "x86_64-pc-windows-gnu"
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
あとはいつもどおり Cargo.toml
と src/lib.rs
を書いていくだけとなります!
内容はgdnativeクレートの公式( godot-rust/godot-rust: Rust bindings for GDNative )から拝借しました。
[package]
name = "hello_godot"
version = "0.1.0"
edition = "2021"
[dependencies]
gdnative = "0.9.3"
[lib]
crate-type = ["cdylib"]
use gdnative::prelude::*;
#[derive(NativeClass)]
#[inherit(Node)]
pub struct HelloWorld;
#[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 --release
等でコンパイルしましょう。 target/x86_64-pc-windows-gnu/release/hello_godot.dll
が目的のライブラリになります。
動作確認
Godotに hello_godot.dll
を読み込ませて、正しく動作することを確認します。
Godotのプロジェクトフォルダに hello_godot.dll
をコピーし、以下の手順でシーンを用意していきます。(チュートリアルは行ったものと仮定して大雑把に書きます)
- Godotのプロジェクトを用意し、
Node
型のルートノードを含むシーンを用意します - 動的ライブラリを導入するためにファイルシステムタブで右クリック→
新規リソース
→GDNativeLibrary
から.gdnlib
ファイルを用意(.tres
から変更。.tres
との違いはあまり無いみたい...?)。名前は好きなものでよいのでここではデフォルトのnew_gdnativelibrary.gdnlib
とします。 - 作成した動的ライブラリを対応するプラットフォームに指定します。指定後必ずインスペクタにある保存ボタンでnew_gdnativelibrary.gdnlibファイルを保存してください。
.dll
なのでWindows 64
に入れます。
- ルートノードにスクリプトをアタッチします。言語を
NativeScript
、クラス名をHelloWorld
にしておきます。Node.gdns
のようなファイルが生成されます。
- アタッチしたスクリプトのインスペクタにある
NativeScript
→Library
に先程用意したnew_gdnativelibrary.gdnlib
を指定します。その後、Node.gdns
の方を保存します。(Ctrl + s で保存されます)
-
Node.tscn
を保存し、メインシーンに指定します。
この状態で現在のシーンを実行し、コンソールに Hello, world.
と出ていれば成功です!
WebAssembly化するための開発環境整備
ここからが本題ですが私もorion78frさんの手法を真似ただけですので詳細がよくわからない部分が結構あります。あと本題とは言いましたがかなりニッチなパートであることは理解しています。誰かの手助けになれば幸いという気持ちで書いています。
本筋としては、Rustツールチェインの内emscriptenを使ったwasm化が行えるwasm32-unknown-emscripten
を使用します。emscriptenを使用しないwasm32-unknown-unknown
の方でのやり方はまだないようです。
依存関係追加
まずは依存関係を追加していきます。emscriptenを使用するためにemsdkを入れていきますが、emsdk関連の知識が要求されることはないです。(というか僕もわかってない)
apt
, git clone
, rustup
コマンドを実行していきましょう。以下ホームディレクトリに移ってから行っています。
ホームディレクトリ以外でも構いませんがその場合は間違えないように適宜読み替えてください。
cd ~
sudo apt install python3
git clone https://github.com/emscripten-core/emsdk.git
rustup target add wasm32-unknown-emscripten
git clone
で取ってきた emsdk
フォルダに移ります。最新のファイルが入るように git pull
コマンドを打ち、 emsdk
の必要なバージョンを取ってきます。
cd emsdk
git pull
./emsdk install 2.0.17
./emsdk activate 2.0.17
emsdk
のバージョンは 2.0.17
である必要があります。すべてのバージョンを試したわけではないですが、現在の最新バージョンでは条件が足りないのかコンパイルできませんでした。
activate
を実行すると最後にこう書いてあります。
- Configure emsdk in your shell startup scripts by running:
echo 'source "~/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile
emsdk_env.sh
は環境変数などをアクティブにするためのコマンドが含まれていますので、とりあえず .wasm
をコンパイルする際には予め実行しておくようにしましょう。
source "~/emsdk/emsdk_env.sh"
このコマンドを実行後に、emsdkフォルダの存在位置が $EMSDK
変数で参照できるようになります(本記事では $EMSDK == ~/emsdk
)。そのため、以降本記事でも $EMSDK
変数を使用していきます。
ここからがちょっとというかかなり大変なところになります。頑張りましょう。
wasm-opt
への細工
まずは、wasmファイルを最適化するためのwasm-opt
というアプリケーションに対して、 -all
というオプションを渡すための細工をします。
オリジナルのものを別なファイルに保存しておき、コマンドの中身を書き換えます。(強引...)
mv upstream/bin/wasm-opt upstream/bin/wasm-opt-bak
#!/bin/bash
$EMSDK/upstream/bin/wasm-opt-bak $@ -all
ファイルに実行権限を与えます。
chmod +x $EMSDK/upstream/bin/wasm-opt
リンカー emcc-test
の作成
デフォルトのリンカー( emcc
)ではなく、細工を施したリンカーを用意します。orion78frさんの手法そのままに emcc-test
という名前にしています。場所はどこでもいいので個人的に好きな ~/bin/
以下に置くこととします。(PATHを通してね)
#!/bin/bash
arr=()
for f in "$@"; do
if [[ "$f" == *.rlib ]]; then
#echo Extracting $f
ar --output "$(dirname $f)" -x $f
ar -t $f | grep .o | while read o; do
fo=$(dirname $f)/$o
#echo File $fo
arr+=("$fo")
done
else
#echo Passing arg $f
arr+=("$f")
fi
done
emcc ${arr[@]}
ar
コマンドはアーカイブを作成するコマンドらしい...? 正直 emcc
を呼ぶ前に特殊なことをしているということしかわからない...まぁ動いてしまえばこっちのものです!
emcc-test
にも実行権限を与えておきましょう。
chmod +x ~/bin/emcc-test
環境変数の用意
あともう少しです!コンパイル前に環境変数をexportしておきます!( config.toml
に書く方法ではうまく行かなかったため直接exportしています。)
export C_INCLUDE_PATH="$EMSDK/upstream/emscripten/cache/sysroot/include/"
export EMMAKEN_CFLAGS="-s SIDE_MODULE=1 -shared -Wl,--no-check-features -all"
C_INCLUDE_PATH
はemsdkにあるライブラリを使いたいのですからわかるのですが、EMMAKEN_CFLAG
の方はビルド用のおまじないみたいです()
./cargo/config.toml
への追記
.dll
のとき同様、コンパイル設定を書いていきます!コンパイルまであと少し!
以下のように追記してください
[build]
target = "x86_64-pc-windows-gnu"
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
+ [profile.release]
+ opt-level = "s"
+ overflow-checks = false
+ debug-assertions = false
+ lto = true
+ panic = "abort"
+ [target.wasm32-unknown-emscripten]
+ linker = "emcc-test"
+ rustflags = "-C link-args=-fPIC -C relocation-model=pic -v"
コピペ用
[build]
target = "x86_64-pc-windows-gnu"
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
[profile.release]
opt-level = "s"
overflow-checks = false
debug-assertions = false
lto = true
panic = "abort"
[target.wasm32-unknown-emscripten]
linker = "emcc-test"
rustflags = "-C link-args=-fPIC -C relocation-model=pic -v"
コンパイル
ここまで正確にできていれば、あとはコンパイルするのみです!
cargo build --release --target=wasm32-unknown-emscripten
実行後、目的の target/wasm32-unknown-emscripten/release/hello_godot.wasm
が生成されています。
動作確認
最後に動作確認しましょう! .wasm
の方はHTMLにエクスポートして確かめます。
.dll
のときと同様に、 new_gdnativelibrary.gdnlib
に hello_godot.wasm
ファイルを指定します。今回は HTML5 -> wasm32
に指定しましょう
プロジェクト
→ エクスポート
からエクスポートを行います。プリセットに HTML5
を追加し、Export Typeに GDNative
を指定した後、「プロジェクトのエクスポート」をクリックしてエクスポートします。エクスポート先では新たなディレクトリを作成してエクスポートすることを推奨します。また名前も index.html
にしておくと楽です。
その後例えば python3 -m http.server
などのコマンドを使用してディレクトリをホストし、ブラウザを起動してアクセスしてみます。デベロッパーツールの出力に Hello, world.
と表示されていれば無事にコンパイルが完了しています!
シューティングゲーム制作編
以降、小話を一つと考察を行って本記事を締めたいと思います。
オブジェクトプール
シューティングゲームってゲームの中では(僕が今回作ったようなレベルのものならば)簡単に作れそうな気がしませんか?UnityやGodotのように予め当たり判定用のコライダーのような仕組みがあるゲームエンジンならそのように思えるかもしれません。
しかし過去のプログラム経験が浅かった自分は一度挫折したことがあります。それはメモリ管理への意識が薄かったからでした。
初心者だった自分は敵に当たった弾や外に出ていった弾を free
( destroy
や del
とも。)してしまっていたのです!!!毎フレーム無限にメモリ確保と解放を行っていたのでは、いくら高性能な言語でも遅くなってしまいます。ましてや、当時の自分は pygame
でゲームを作っていたのですが、限界は弾を円形に飛ばしたところですぐに来てしまいました。
これを解決する手法は至ってシンプルです。弾を「使いまわせ」ば、メモリ解放や確保の機会が極端に減ります。この手法を「オブジェクトプール」と呼びます5!
挫折の経緯があって自分の中ではとりあえずオブジェクトプールを実装できればシューティングゲームは作れるのではないかという認識になっています。(もっと作り込んでいくとなると話は別ですが...)
今回のGodot+Rustによる制作では、Godotのシグナル機能がオブジェクトプール制作にとても役立ちました。
#[export]
fn cartridge_fallen(&self, owner: &Area) {
// 親ノードは現在ステージで、ステージの管理下から外す
unsafe {
let parent = owner.get_parent().unwrap().assume_safe();
parent.remove_child(owner.assume_shared());
}
// 所有権に関してユニークと仮定 (areaにはポインタが入ります)
let area = unsafe { owner.assume_unique() };
// ポインタ(あるいはアドレス)をVariantにキャストしてシグナルとして送信
owner.emit_signal("collect", &[Variant::from_object(area)]);
}
#[export]
fn collect_bullet(&mut self, _owner: &Area, bullet_var: Variant) {
// 動的な変数Variantの値をAreaのポインタにキャスト
let bullet_area: Ref<Area, Unique> =
unsafe { bullet_var.try_to_object().unwrap().assume_unique() };
// マガジンに貯める
self.mut_magazine().charge_bullet(bullet_area);
}
シグナルを受け取ったら弾を格納する配列に戻せば、それはオブジェクトプールです。今回は自機弾、敵であるエイリアン、敵の弾をオブジェクトプールで実装しました。Unityと比べるとGodotは仕組みがシンプルで扱いやすいように感じたのですが、特にシグナルという仕組みはUnityのイベントハンドラ等と比べてもわかりやすいように感じます。
NULL安全なRustはゲーム制作向き言語なのか?6
この節は完全にポエムになりますが、ゲームをデプロイしたのでポエムる資格はあるだろうということでポエムって行きます!
...と思いましたが長いので折りたたみます
推敲を全くしていない駄文
今回GodotとRustを連携させてゲームを作ったわけですが、当然大変でした!!
なぜかって、継承等が存在するクラス型オブジェクト指向をGodotはめちゃくちゃ使っているのに対して、Rustは真っ向からクラス型オブジェクト指向を否定する言語ですし、かみ合わないのは当然です!
しかしgdnativeクレートの作りが良いのか、Godotの仕組みが良いのかわかりませんが、むしろノードたちやそのAPIとそれを操るロジックの分離という観点ではそこまで不快な感じはまったくなかったです。
そして書いているうちにGodotだけの問題ではないんじゃないかという以下の問題に気づきました。
- グローバル変数しかないのか?というようなunsafeやunwrapの嵐(これはGodotのせいであるとも言える)
- まるでシングルトンパターンかのように構造体に連なる変数の数々
なぜこうなってしまったのか...それは「ゲームは組合せ爆発を楽しむものだ」からではないか??と自分は考察しました。
情報量が多いほど楽しいのがゲームです。
例えばポケモン。色違いのポケモンというのはめちゃくちゃ レア ですよね?例えばアチーブメント機能。多くの実績を解除したというのは多くの経験をしたということなので珍しいものです。珍しいということはそれだけ情報量が多いといえます7。情報量の多さは組み合わせ爆発につながります。将棋や囲碁なんかは、まさに組合せ爆発をAIがどう攻略するか?みたいな話が出ますよね。
情報量や組み合わせが多いと条件分岐は複雑になりがちで、フローチャートに起こしにくくなります。麻雀を実装しよう!ってなると楽しそうに聞こえますが、麻雀のフローチャートを描けって言われたら「無理だろ!」ってなります。でも、ゲーム制作というのは究極的にはこのフローチャートをどう実現するかということに等しいです!(強引)
麻雀を打つときに脳内にフローチャートを描いたりはしないのと同様、ゲーム制作をしている最中はおそらく多くの「仮定」を置くことで、組み合わせ爆発に対抗し、制作を楽にしていると言えます。
例えば「交通止めをしているNPCを横切るときに主人公がモンスターの卵を孵化させる」ようなことはないだろうみたいな仮定です。こんな珍しい現象をいちいち気にしていたらゲームなんて作れません。ゲーム制作は多くの仮定の元で成り立っているのでしょう。
ここでRustやElmなどを引き合いに出します。Elmアーキテクチャは強引に言えば「最初からすべての場合に対応できるようにすればバグは生まれないよね」というようなものですし、Rustは持ち前のOption型やResult型を使いつつ、グローバル変数などを禁止しできるだけ「起きうる状況を仮定ではなく確定させる」ような特徴を持っていると言えるでしょう。
もちろん、そのゲームを"完全"なものとしたいならば、RustやElmのこういった性質は喉から手が出るほど欲しくなります。ここまでロジックエラーとその他のエラーをごちゃまぜに書いてきましたが、RustやElmは時としてこのロジックエラーまでもを防ぐ可能性を持っているスーパー言語ですから。
しかしそれが「仮定駆動開発」とは正反対であるというのは明らかです。こういう場面ではリフレクションを持っている言語や動的型付けの言語のほうが楽に事が進みます。
...ふぅ...
つまり結論ですが、当たり前のようなことですが開発速度と正確性はトレードオフであり、「すべてを」Rustで行うゲーム開発は苦行であるというのは言い逃れができそうにないということが言いたいことになります。
じゃあどうしろと?
今回、GUIなどはPythonライクな言語であるところのGDScriptで書いたのですが、GDScript良いですね。ゲーム制作に必要十分な言語というような感じがしました。でもRustacean的には物足りない部分があったのも事実です。
結局のところ、両方を上手に使い分けるのが一番開発効率が良さそうです。パフォーマンスや正確性を求められる場所はRustで、仮定駆動開発をしたい箇所はGDScriptで書くようにするのが、Godotで開発する場合は最適解ではないかと思います。次回作はそのように作ってみたいです。
終わりに
とは言ったものの、まだECSをやたら主張するbevyとAmethystにしっかりと入門できていなかったので、次回の記事はECSを通してRustによるゲーム制作の可能性の模索を続ける8ようなものになる気がします。
Unityを含め、ゲーム制作のベストプラクティスはなんだろうっていつも妄想しているので、そういった関連のコメントを頂けると大変嬉しいです! ここまで読んでいただきありがとうございました!
参考・引用等
- Godot EngineからRustを呼ぶ https://zenn.dev/kawaxumax/articles/e0dedf3f6d4219
- godot-rust/godot-rust: Rust bindings for GDNative
- Support wasm32-unknown-emscripten target · Issue #647 · godot-rust/godot-rust https://github.com/godot-rust/godot-rust/issues/647
- Your first game — Godot Engine (stable) documentation in English
- Scenes and nodes — Godot Engine (stable) documentation in English
- 最初のゲーム — Godot Engine (stable)の日本語のドキュメント (リンク切れ) https://docs.godotengine.org/ja/stable/getting_started/step_by_step/your_first_game.html
- シーンとノード — Godot Engine (stable)の日本語のドキュメント (リンク切れ) https://docs.godotengine.org/ja/stable/getting_started/step_by_step/scenes_and_nodes.html
シューティングゲームに使用させていただいた素材
制作環境
- Godot Engine https://godotengine.org/
- Rust https://rust-lang.org/ja
- VSCode https://code.visualstudio.com/
- MagicaVoxel https://ephtracy.github.io/
楽曲等
- メインBGM : 8bit11 魔王魂 (森田公一氏) https://maou.audio/
- 効果音各種: 無料効果音 https://taira-komori.jpn.org/freesound.html
-
「3D」をタイトルに付けましたが「ほぼ2Dじゃない?」というようなゲームなのである意味タイトル詐欺のようにも見えますが、3Dモードで作ったので誰がなんと言おうと3Dゲームです... ↩
-
日本語のドキュメントも存在したのですが、記事執筆中に404になってしまったので代替として英語のドキュメントのURLを載せています。 ↩
-
アクティベーションが必要じゃないかって?そんなゲームエンジンがあるんですか? ↩
-
一応PowerShellなどWindowsのみの環境での構築も図ってみたのですが、clangあたりで面倒になって結局wslで行うことにしました。 ↩
-
余談ですが、東方紅魔郷に登場する咲夜さんも画面の外に出たナイフは回収して使いまわしているそうです。人力オブジェクトプール... ↩
-
この節の作業中にフランちゃんのテーマを聞いていたのでオマージュしました。シューティングゲーム繋がりということで...(レベルが違いすぎますが) ↩
-
そういう傾向があるというだけで正確な話はしていません、ポエムなので許して...情報理論とか情報のエントロピーとか確率理論とかやってる人からみればむしろ誤りな解説かもです ↩
-
もっとも、GodotもECSでしたし、bevyに触れてみたところ結局シングルトンパターンで頑張っているような印象を受けたので、Rustのゲーム制作にまつわる大変さを軽減できるものであるのかどうかと聞かれると今は懐疑的になっていますが...いや!やってから考えるべきですね!精進します! ↩