Rustといえば高速実行、高安全性、そして並行性を達成するということを目的とした言語ですが、それらにものすごくぴったりな用途が存在します。そう「ゲームプログラミング」です(安全性はゲームプログラミングに限った話ではないですが)。
というわけでRustでゲームエンジンとゲーム作ってますというお話をします。というより、それらの中で便利だと思うor裏技的に活用してるRustの機能を紹介していく感じになります。
ゲームエンジンの詳細については別のアドベントカレンダーの同日記事を参照していただくとして、とりあえず本題に入っていきます。
traitによる詳細な可視範囲の設定
ゲームを製作するにあたって、勉強とかもろもろを兼ねて新規に内製エンジン"Interlude"を並行して開発しているのですが(いずれcrates.ioにあがるとおもいます。たぶん)、そのなかでtrait
を活用(駆使)してクレート内にのみ見える内部メソッドみたいなものを定義しています。
現在のRustでは全体に公開(pub
)か同一モジュール外には非公開かの可視性しか指定できませんが、Interludeではtrait
を使用してそれをクレート外にエクスポートしないことによりクレート外から内部メソッドにアクセスできないようにしています。これが正攻法かどうかはわからないのですが(もっと良い方法があったら教えてください...)。
mod a;
pub use self::a::Data;
mod internals { pub use super::a::*; }
use super::internals::*;
// Data::getは外からみえるようにしたいけどData::newは外からみえて欲しくない(でも同一クレートの他modからは見えてほしい)
// -> DataInternalsというトレイトを定義してそれをクレート外にエクスポートしなければ中でしかみえなくなる
pub struct Data(u32);
impl Data { fn get(&self) -> u32 { self.0 } }
pub trait DataInternals : Sized { fn new(v: u32) -> Self; }
impl DataInternals for Data { fn new(v: u32) -> Self { Data(v) } }
enumと&mut selfを用いてオブジェクト死亡処理を効率よく行う
制作中のゲームのほうではenum
でオブジェクトの状態を管理するようにして、アップデート処理で&mut self
を受け取るようにしています。こうすることでオブジェクトの状態を綺麗に管理することができ、「死亡中はこのメンバにさわってはいけない」みたいな不安全さも取り払うことができて、さらにインプレースでの処理になるので効率もよくいいことだらけです。また、ゲームのほうではアップデート処理を並列化するのにRayonを使用しているのですが、これとの相性も良いです。ただself
を直接書き換えるので見栄えとしてはそんなによろしくないというか。
例として、次のようにオブジェクトを定義するとします。
#[derive(Clone)]
enum GameObject { Free, Living(u32), Garbage(u32) }
impl GameObject
{
fn update(&mut self, require_to_die: bool)
{
if let &mut GameObject::Living(bx) = self
{
if require_to_die { *self = GameObject::Garbage(bx); }
}
}
}
updateでrequire_to_dieがtrueの状態でわたってくると死亡状態になるようにします。オブジェクトの後始末に必要な情報も一緒に渡す必要があるのでu32を渡しています。
これを管理するほうですが、次のようにします。
let mut objects = [GameObject::Free; MAX_GO];
...
objects.par_iter_mut().for_each(|o| o.update(...));
for go in objects.iter_mut()
{
match go
{
&mut GameObject::Garbage(bx) => { 後始末コード; *go = GameObject::Free; },
_ => ()
}
}
par_iter_mut().for_each(...)でループを回して、そのあとにもういちどforを回してGarbageを後始末しつつFreeに変換しています。
最終的に普通にforで回すのでupdateと一緒にしてしまってもいいんじゃないかと思わなくもないのですが、updateの中身が激重とかだと天と地ほどの差が出るんじゃないかと思います(期待)。ここに出てくる後始末コードというのはどう頑張ってもシングルスレッドでしか動かせないような処理(メモリ解放とか)をするコードで、これもupdate内でやってもいいのですがロックにかかる負荷を少しでも減らそうという考えです。
おわりに(雑感)
ゲーム制作においてGCは動作負荷を予測できなくて予期しないプチフリーズを引き起こしたりするからできるだけ切るようにする、というのはずっと言われてきたことではありますが、ぶっちゃけC#とか普通に使われてるし今ではもうそこまで気にしなくてもいいような気がRustはGCがなくその心配がないので、性能にかなり信頼を寄せてプログラムを書くことができたような気がします。ただ、GCがない分メモリ管理に重点を置く必要が出てくるのでそこだけ見れば普通に生産性は落ちます。ただ、その落ちた生産性を上回る(かもしれない)くらいの便利な機能(パターンマッチとか)が豊富なので普通にC++で開発するよりはかなり精神的に良いプログラミングができると思います。