Rustでゲームプログラ厶を組む場合、Entity Component System(以下ECS)による設計が有力な候補となります。
RustのECSライブラリ/フレームワークはいくつかありますが、抽象化の恩恵を得るために、内部はダイナミックな機構が使われていることが多いようです。
しかし、せっかくゼロコスト抽象化を謳っているRustなので、スタティックなECSを作りたい!と思い立って、作り始めました。
そして、だいたい形になったので、今回はその紹介をさせていただきます。
できたもの
先に、できたものの見た目を紹介します。
例えばこんなコンポーネントを作ったとして、
struct Input(f32,f32); //アナログコントローラ入力
struct AI{ state: i32 }; //敵のAI とりあえず例として、何かのステートを持つ
struct Velocity(f32,f32); //速度 x,y
struct Position(f32,f32); //位置 x,y
下記の様に、ワールドを定義、生成します。
world!(
//型名
World {
//コンポーネント型を列挙
Input,
AI,
Velocity,
Position,
}
}
//ワールドを生成
let world = World::default();
エンティティーの追加
//プレイヤーキャラ(エンティティ)の作成
add_entity!(
world;
Input::default(),
Velocity::default(),
Position::default(),
);
//敵(エンティティ)の作成
add_entity!(
world;
AI::default(),
Velocity::default(),
Position::default(),
);
システムの定義と呼び出し
//ゲームループ
while !game_end() {
//Inputを参照してVelocityを計算するシステム
system!(
world,
//ラムダ式風の書式で、
//|エンティティIDを受け取る変数, 更新対象の変数:型, 参照する変数:型, 参照する変数:型...|
|entity_id, vel:&Velocity, input:&Input| {
//inputの5倍を速度とする
let mut new_vel = vel.clone();
new_vel.0 = input.0 * 5f32;
new_vel.1 = input.1 * 5f32;
//計算後の値を返す
new_vel
}
);
//AIからVelocityを計算するシステム
system!(
world,
|entity_id, vel:&Velocity, ai: &AI| {
let mut new_vel = vel.clone();
//aiの情報を使って、new_velを決める
// ... 何かのロジック ...
//計算後の値を返す
new_vel
}
);
//VelocityからPositionを計算するシステム
system!(
world,
|entity_id, pos:&Position, vel:&Velocity| {
//位置に速度を加算
let mut new_pos = pos.clone();
new_pos.0 += vel.0;
new_pos.1 += vel.1;
//計算後の値を返す
new_pos
}
);
}
こんな感じになりました。
上記の例は、システムが「同じエンティティーIDを持つコンポーネントだけを参照する場合」ですが、もっと複雑なロジックを書くこともできます。
中身の説明
では中身の説明に入っていきます。
まず試しに、素直な実装でECSを組んでみると、こんな感じになると思います。
//コンポーネント1種類を管理するコンテナを作って
struct ComponentContainer<T> {
components: Vec<T>,
//管理情報など
}
//ワールドの定義
struct World {
inputs: ComponentContainer<Input>,
ais: ComponentContainer<AI>,
velocities: ComponentContainer<Velocity>,
positions: ComponentContainer<Position>,
}
ここでまず、構造体のフィールド名と、コンポーネント型を1対1で対応させることになります。
positions: ComponentContainer<Position>,
^^^ここと ^^^ここが
同じになるので、わざわざ書くのが面倒な気持ちになります。
特にコンポーネントの種類が増えてくると、煩わしいですね。
かといってタプルを使って
type World = (
ComponentContainer<Input>,
ComponentContainer<AI>,
ComponentContainer<Velocity>,
ComponentContainer<Position>,
);
としても、使うときに、えーとVelocityは何番目だったっけ・・・てなりますね。
なんとか、型名だけを覚えていれば使えるようにできないものでしょうか?
concat_idents (https://doc.rust-lang.org/std/macro.concat_idents.html) を使えばいけそうな気もしますが、型名をidentityにつなげることは無理な様でした。
そこで、考案したのがこちら
macro_rules! recursive_tuple {
( $t: ty ) => {
($t, ())
};
( $th:ty, $( $tt: ty ),+ $(,)? ) => {
($th, recursive_tuple!($($tt),+))
};
}
これは、例えば
recursive_tuple!(i32, f32, u32)
と書くと
(i32, (f32, (u32, ())))
こんな風に展開されます。(再帰的なタプル)
これだと、例えば
type IntFloatUint = recursive_tuple!(i32, f32, u32);
let ifu = IntFloatUint::default();
としたとして、
//i32を参照するには
ifu.0
//f32を参照するには
ifu.1.0
//u32を参照するには
ifu.1.1.0
このように、再帰的に 1.
を繰り返すことで、目的の型を持つデータを参照できます。
そして、型を指定して参照するようにしたい場合は、ジェネリックなtraitを用いて、下記のようにできます。
trait TypeRef<T> {
fn type_ref(&self) -> &T;
}
impl TypeRef<i32> for IntFloatUint {
fn type_ref(&self) -> &i32 {
&self.0
}
}
impl TypeRef<f32> for IntFloatUint {
fn type_ref(&self) -> &f32 {
&self.1.0
}
}
impl TypeRef<u32> for IntFloatUint {
fn type_ref(&self) -> &u32 {
&self.1.1.0
}
}
このimpl をマクロで定義できるようにしてみます。
macro_rules! impl_typeref {
( $torg:ty, $t: ty ) => {
impl TypeRef<$t> for $torg {
fn type_ref(&self) -> &$t {
&self.0
}
}
};
( $torg:ty, $th:ty, $( $tt: ty ),+ $(,)? ) => {
impl TypeRef<$th> for $torg {
fn type_ref(&self) -> &$th {
&self.0
}
}
$(
impl TypeRef<$tt> for $torg {
fn type_ref(&self) -> &$tt {
self.1.type_ref()
}
}
)+
impl_typeref!{ recursive_tuple!($($tt),+), $($tt),+ }
};
}
説明します。
impl_typeref!(IntFloatUint, i32, f32, u32)
と書くと、以下のように再帰的に展開されます。
//展開 第一段階
impl TypeRef<i32> for IntFloatUint {
fn type_ref(&self) -> &i32 {
&self.0
}
}
impl TypeRef<f32> for IntFloatUint {
fn type_ref(&self) -> &f32 {
self.1.type_ref()
}
}
impl TypeRef<u32> for IntFloatUint {
fn type_ref(&self) -> &u32 {
self.1.type_ref()
}
}
impl_typeref!( recursive_tuple!(f32, u32) ); // (f32, (u32, ()))に対して再帰
//展開第二段階
impl TypeRef<f32> for (f32, (u32, ())) {
fn type_ref(&self) -> &f32 {
&self.0
}
}
impl TypeRef<u32> for (f32, (u32, ())) {
fn type_ref(&self) -> &u32 {
self.1.type_ref()
}
}
impl_typeref!( recursive_tuple!(u32) ); // (u32, ())に対して再帰
//第三段階
impl TypeRef<u32> for (u32, ()) {
fn type_ref(&self) -> &u32 {
&self.0
}
}
そうすると、下記のように型を指定してアクセスできるようになります。
let i = TypeRef::<i32>::type_ref(&ifu);
これもちょっと煩わしいので、マクロにします。
macro_rules! typeref {
($e:expr, $t:ty) => {
TypeRef::<$t>::type_ref(&$e);
};
}
そうすると
let i = typeref!(ifu, i32);
このように「型を指定するだけでフィールドを参照できるタプル」を作ることができます。
なお、型は同じものが2つ使われていないことが前提です。
もし使おうとすると、impl Traitのところで二重定義のエラーがでます。
これをECSのコンポーネントの管理に使うことで、最初に紹介した様な、スタティックにコンポーネントにアクセスするECSの仕組みができました。
ソースはこちらで公開しています(まだ開発中です)
https://github.com/mas-yo/static_ecs
これを使ってサンプルゲームも作っています。
https://github.com/mas-yo/rust-ecs-game
さいごに
私もまだまだ勉強中なので、それならこんなクレートを使えば便利だよ!とか、ここはこうしたらいいんじゃない?とかあったら、教えてください。
いやーRustって、本当に、面白いものですね。
ではまた、お会いしましょう♪