Rustでゲーム開発 - 個人的まとめ
2020/12/05 (updated 2020/12/06)この記事は TUT Advent Calendar 2020 5日目の記事です。
豊橋技術科学大学 M1のrekzと申します。普段は研究でテキスト処理とかのプログラムを書きながら,息抜きにゲームとか作ったりしています。 と言うと嘘になるかもしれない。最近はよく寝ています。すぴー
私のお気に入りのプログラミング言語にRustというものがありまして,これがなかなかストイックではありますがうまく使えれば高速で安全で再利用性の高い最強のプログラムが書ける最高のプログラミング言語です(主観)。Rustの技術的詳細についての説明は省きますが,普通のプログラミングに飽きた物好きな人たちが良く使っている言語という印象があります(偏見)。実社会でもパフォーマンスと安全性の両方が重視される分野では採用されていたりしますね。
ゲーム開発でもパフォーマンスは重要だろ!ゲーム開発にこそRustだ!ということで,Rustでゲーム開発を行うためのライブラリや環境の整備が各所で進められています。Rust Game Development Working Group や Are we game yet? といったページでは,Rust製のゲームやライブラリ,教材などの情報がまとめられています。今のところ,UnityやUnreal Engineのような統合開発環境的なゲームエンジンがあるわけではありませんが,ゲーム開発用のクレート(Rustではライブラリをパッケージにしたもののことをクレートと呼びます)は大量に存在しています。以下,私がチラッと触ったことのあるライブラリや注目しているライブラリを少しだけ紹介していきます。
Piston
PistonはRust製ゲーム開発ライブラリとしては昔から存在している老舗のライブラリです。正確には,Pistonというひとつのクレートで機能が完結しているわけではなく,PistonDevelopers organization にあるような大量のクレートから必要な機能を選んで使う感じです。公式には A modular game engine written in Rust と表現されていますね。
特に,ウィンドウの初期化・描画・イベント処理といった基本的な機能をまとめた便利クレートとして piston_window が用意されています。機能としてはクラシックな感じのシンプルなAPIで,メインループをユーザーが書く必要があります。公式のサンプルコードを以下に引用します。
extern crate piston_window;
use piston_window::*;
fn main() {
let mut window: PistonWindow =
WindowSettings::new("Hello Piston!", [640, 480])
.exit_on_esc(true).build().unwrap();
while let Some(event) = window.next() {
window.draw_2d(&event, |context, graphics, _device| {
clear([1.0; 4], graphics);
rectangle([1.0, 0.0, 0.0, 1.0], // red
[0.0, 0.0, 100.0, 100.0],
context.transform,
graphics);
});
}
}
複雑なグラフィック処理やサウンド,ゲームオブジェクト管理などは別途ライブラリを使うか自分で実装します。上記のorganizationなどを探すとPiston用のライブラリが色々あります。とはいえ,ちょっと凝ったことをしようとすると依存関係の深い沼に嵌りやすい印象があるので,あくまで簡単な描画処理を行うための基礎部分として使うのがいいかもしれません。
Amethyst
Amethystは Entity Component System(ECS) モデルを採用したデータ指向設計のRust製ゲームエンジンです。
ECSやデータ指向といった言葉は説明が難しいのですが,最近ではUnityの新しい中核基盤であるDOTSが採用していることで話題になっていたりします。"Unity ECS"などで検索すると解説記事がたくさん出てくるので本記事では詳しく説明しませんが,雑に言うとオブジェクト指向的な考え方とは違った設計を採用することでメモリ効率やプログラムの並列性を向上させることができるというものです。
何を隠そう私はこのECSが大好きで,2年くらい前からAmethystを使ってゲームを作っていたりします。ECSを触りたいだけならUnity DOTSでもいいと言えばいいのですが,Rustはもともと低レベルなメモリ制御ができることやデータとアルゴリズムを分離させる書き方ができることなどから,特にECSと相性が良いと思っています。
また,AmethystはECSエンジンだけでなく,Vulkan/Metalベースの高機能な2D/3Dレンダリングエンジンや,各種入力デバイスからのデータ取得,非同期なアセット読み込み機構など,ゲームエンジンに必要な様々な機能を備えています。ユーザーが書くコードはそこそこ長くなりますがいろいろな設定が可能で,実行状態の管理はAmethystが行います(メインループは書きません)。公式ドキュメントからSystemの記述例の一部を以下に引用します。
struct MakeObjectsFall;
impl<'a> System<'a> for MakeObjectsFall {
type SystemData = (
WriteStorage<'a, Transform>,
ReadStorage<'a, FallingObject>,
);
fn run(&mut self, (mut transforms, falling): Self::SystemData) {
for (transform, _) in (&mut transforms, &falling).join() {
if transform.translation().y > 0.0 {
transform.prepend_translation_y(-0.1);
}
}
}
}
このプログラムではTransform
やFallingObject
といった型がコンポーネントとして使われていますがこれらは別に定義されます。AmethystではコンポーネントはComponentトレイトを実装した普通の構造体です。Unity DOTSでの書き方と見比べてみると面白いかもしれません。
なお,Amethystバージョン0.15におけるECSエンジンの正体はSpecsというライブラリであり,現在,Legionという別のECSエンジンへの移行作業が進められています。移行が完了するとSystemの記述含め多くのAPIに変更が入る予定です。さらに,Amethystで実装したゲームをブラウザ上で動かすためのWebAssembly/WebGL対応も進められているようです。
ちなみに,最近出てきた別のRust製ECSゲームエンジンとしてBevyがあります。これについてはまだちゃんと触っていないのでよくわかりませんが…。
glow
glowは様々な環境で同じコードでOpenGLを呼び出すためのOpenGL/WebGLバインディングです。
これはゲームエンジンではないです。何らかの方法でウィンドウの初期化を行い,OpenGLのコンテキストを生成した後に使うライブラリで,最大の特徴はWebGLに対応していることです。つまり,ネイティブとウェブ上の両方で同じOpenGLアプリケーションを動作させることができます。
glowを使うためにはまずウィンドウの初期化を行う必要がありますが,これにはwinitなどを使えばOKです(SDL2などでもいいですが,どうせならpure Rustにしたいですよね)。winitはnative/web両対応のウィンドウ初期化ライブラリで,ウィンドウイベントのハンドリングも行うことができます。webの場合は単にcanvas要素をウィンドウとして扱うことができます。nativeの場合はウィンドウを初期化した後にOpenGLのコンテキストを生成しなければならないので,winitにOpenGLコンテキスト生成機能を追加したライブラリであるglutinを代わりに使用します。実際のプログラムは公式のサンプルコードのようになります。初期化とイベントループはターゲットによって切り替えられていますが,OpenGLを叩くコードは共通になっています。
ネイティブ・ウェブ両対応のGUIライブラリはもっと高レベルなものもありますが,ゲーム開発には向いていなかったり,あまりこなれていない気がしたりするので,OpenGLを叩くレベルからコードを書いてもいいならglowは選択肢になると思います。より低レベルなものだとrendyやwgpu-rs,gfx-rsなどがありますが,これらは直接使うためのものではない気がする…。
Nannou
Nannouはクリエイティブ・コーディングのためのRust製フレームワークです。ゲーム開発とはちょっと毛色が違いますが,まあゲーム開発もできるということで紹介します。公式ページにはProcessingやOpenFrameworks,Cinderの影響を受けたと書かれており,最小限のコードでウィンドウを表示できる基本機能や高度なグラフィック処理機能などを備えています。OSC(Open Sound Control)に対応してたりするところもクリエイティブ・コーディング用らしい感じがします。もちろんシェーダーも書けます。
非常に親切な公式ドキュメントがあるのでそれを読むのが手っ取り早いと思いますが,一応最小限のコード例を以下に示します。基本的なAPIの雰囲気が分かるかと思います。
use nannou::prelude::*;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
}
fn model(app: &App) -> Model {
let _window = app.new_window().view(view).build().unwrap();
Model { _window }
}
fn update(_app: &App, _model: &mut Model, _update: Update) {}
fn view(app: &App, _model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(PLUM);
draw.ellipse().color(STEELBLUE);
draw.to_frame(app, &frame).unwrap();
}
内部状態を持たず,時間やマウス入力などの情報だけで各フレームの描画を行う場合は,描画部分のコードだけを記述する「スケッチ」として実装することもできます。
use nannou::prelude::*;
fn main() {
nannou::sketch(view).run()
}
fn view(app: &App, frame: Frame) {
let draw = app.draw();
draw.background().color(PLUM);
draw.ellipse().color(STEELBLUE);
draw.to_frame(app, &frame).unwrap();
}
ぶっちゃけたところ,私は最近このライブラリを知ったのであまり機能を把握していないのですが,使いこなせるようになると便利だと思うので使っていきたいです。
おわりに
遅刻しました。気づいたらなんかそこそこ書いてた。
いい機会なので,去年豊橋技術科学大学コンピュータクラブから頒布したAmethyst製自作ゲーム"Tofu on Fire"をソースコードごと公開しようと思います。しました。こんなゲームです:
頒布したときのバージョンはAmethyst 0.10.0を使っていたんですが,そのあともこっそり最新バージョンに追従したり変な機能を追加したりしています。自己満足!
今年はなんか全然公開できるもの作れてなかったんで来年こそはという気持ちです。それでは。