🎮

AmethystのECS紹介 ~Rust+ECSのゲームエンジン~

2021/02/07に公開

はじめに

本記事はAmethystというゲームエンジンについて、主にECSアーキテクチャの解説する入門記事です。

  • Amethystは知らないけどRustでECSアーキテクチャのゲームエンジンって気になる!
  • Amethystは知っているが、アーキテクチャに沿った実装手順がいまいちわからない!
    といった方に向けた内容となっています。

いわゆる環境構築をして動かしてみる紹介記事というより、AmethystのECSアーキテクチャについて構造や実装手順を説明する記事となっていますので、あらかじめご了承ください。

また、執筆した2021年2月上旬現在ではAmethystのバージョンは0.15.1となっております。
バージョンによっては一部情報が違う場合もあるのでご注意ください。

Amethystとは

AmethystとはRustで実装されているデータ指向でECSアーキテクチャを採用しているゲームエンジンです。
公式サイトは以下のリンクになります。

https://amethyst.rs/

オープンソースで、リポジトリはこちらのリンクになります。

https://github.com/amethyst/amethyst

開発も盛んで公式ドキュメントも整備されています。スポンサーの企業がついていたり、Discordやreeditに開発コミニュティがあったりと、多くの支持があるのも自分がAmethystを触ってみている要因のひとつです。

AmethystのECSアーキテクチャ

それではAmethystのECSアーキテクチャについて説明します。ECSアーキテクチャ自体の詳しい説明についてはこちらのリンク先を参考にしてください。

https://zenn.dev/rita0222/articles/c22a8367e31b4d5f4eeb

AmethystではEntityは識別子となるメンバを持つstructとして、ComponentとSystemはtraitとして定義されています。任意のゲーム内オブジェクトの機能を考えた時に、Componentというデータ部とSystemというデータに対する作用をtraitとして実装する必要があります。この実装については後述します。

EntityとComponentはWorldという機能で管理されます。World内で、EntityはEntityResと呼ばれるストレージに、Componentはtraitで定義された型のストレージに格納されます。ComponentはEntityと紐付けられており、任意のオブジェクトのComponentを参照する際にこの紐付けが必要になります。次の画像は公式ドキュメントより引用したEntityとComponentの管理図です。


引用元: https://book.amethyst.rs/stable/concepts/entity_and_component.html

Entityに対して、PositionやBottleといったそれぞれ別に管理されているストレージが紐づけられているのがわかるかと思います。Systemの役割はこれらのEntityに紐付いたComponentをイテレートしてゲーム内のルールに基づいた作用を施すことです。図の例であればPositionComponentの座標更新を行ったり、PersonComponentとBottleComponentのデータから特定のEntityを抽出して処理を行ったり、といったような感じです。

図と言葉だけだとちょっと分かりづらいので、コードも含めてEntity、Component、Systemそれぞれについてもう少し説明します。
まずはEntityから説明します。Entityの定義を次に示します。

// Entityの定義

#[derive(Clone, Copy, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Entity(Index, Generation);

impl Entity {
    /// Creates a new entity (externally from ECS).
    #[cfg(test)]
    pub fn new(index: Index, gen: Generation) -> Self {
        Self(index, gen)
    }

    /// Returns the index of the `Entity`.
    #[inline]
    pub fn id(self) -> Index {
        self.0
    }

    /// Returns the `Generation` of the `Entity`.
    #[inline]
    pub fn gen(self) -> Generation {
        self.1
    }
}

Indexはu32の型エイリアスでGenerationはEntityの削除を識別するためのパラメータになります。ゲーム内のオブジェクトを識別するためだけのシンプルな作りになっています。

次にComponentとSystemについて説明します。前述にもあるようにComponentとSystemはtraitの実装を行う必要があります。分かりやすくするために、ゲーム内のヒットポイントを持つプレイヤーを考えたとして、その実装例を下に示します。

// Componentの実装

#[derive(Clone)]
pub struct Player {
    // 適当なメンバ変数
    pub hp: i32,
}

// 適当なメソッド
impl Player {
    pub fn new(hp: i32) -> Player {
        Player {
            hp
        }
    }
    
    // hpを減らす
    pub fn damage_hp(&mut self, value: i32) {
        hp -= value;
    }
}

// Componentのtrait実装
impl Component for Player {
    // Componentが管理されるストレージの型を定義する
    type Storage = DenseVecStorage<Self>;
}

// Playerのオブジェクトを生成する
// Entityと共に生成してComponentを紐付けてWorldで管理する
pub fn create_player(world: &mut World) {
    let mut player = Player::new(100);
    
    // Entityの生成とComponentの紐付け
    world
       // EntityをWorldに追加
        .create_entity()
	// EntityとComponentの紐付け
        .with(player)
        .build();
}
//Systemの実装

pub struct PlayerSystem;

impl<'a> System<'a> for PlayerSystem {
    // SystemからWorldに対しての参照
    type SystemData = (
        // mutableなPlayerのストレージへの参照
        WriteStorage<'a, Player>,
	Entities<'a>
    );

    // ゲームループ中に呼ばれるメソッド
    fn run(&mut self, (mut players, entities): Self::SystemData) {
        // PlayerのComponentと紐付いたEntityに対するイテレーション
        for (player, entity) in (&mut players, &*entities).join {
            player.damage_hp(1);
	    // Playerのhpが0ならば該当のEntityを削除
	    if player.hp <= 0 {
	        entities.delete(entity);
	    }
        }
    }
}

PlayerというComponentとPlayerSystemというSystemを実装を示しました。Playerはhpというメンバをもち、それを減らすdamage_hpメソッドを実装しています。SystemはPlayerのComponentと紐づくEntityを列挙し、呼び続けて0になったらEntityを削除しています。

  • プレイヤーの機能に対してヒットポイントの管理というデータ部
  • ルールに基づくヒットポイントの変動とオブジェクトの削除判定というゲーム内の作用

このように、機能をデータとゲーム内での作用に分解して実装するのがComponentとSystemの手順になります。後はSystemをライフサイクルに紐づける実装を行うことで、これらのECSアーキテクチャが稼働します。(本記事ではその部分の話は省略します。)

以上がAmethystのECSアーキテクチャの説明になります。

おわりに

AmethystのECSアーキテクチャについて構造や実装手順の解説をしてきました。もちろんAmethystで開発を行うには本記事で解説した知識だけでなく、ライフサイクルやAmethyst固有の用語と役割などの理解が必要になります。Amethystでの開発をやってみようと思った方はこちらのリンク先の公式ドキュメントを参考にしてください。

https://book.amethyst.rs/stable/

こちらではAmethystで実装するための手引きに加えて簡単なチュートリアルも用意されています。まずはチュートリアルの実装を進めつつ、手引きを参照するのがおすすめです。日本語の手引きで一回動く物を作ってみたいという方はこちらのリンク先の記事を参考にしてください。

https://qiita.com/takeryo_dayo/items/30431ef95bf7c38642a7

自分はRustの勉強の一環としてRust製のゲームエンジンを色々と調べていてAmethystに行き着きましたが、結果としてRustを学びつつECSアーキテクチャの考え方や構造に触れられているため好感触です。
ちなみに、Rust+ECSアーキテクチャのゲームエンジンはAmethystの他にも存在します。こちらのリンク先の記事でその内のひとつであるBevyというゲームエンジンが紹介されていたので興味のある方はチェックしてみてください。

http://blog.livedoor.jp/mizuki_izuna/archives/24883864.html

本記事がゲーム開発およびRustやECSアーキテクチャの学習のお役に立てば何よりです。

参考文献

最後に参考にさせていただいた文献のリンクを紹介します。

https://zenn.dev/rita0222/articles/c22a8367e31b4d5f4eeb

https://qiita.com/9laceef/items/380ac0944199de7aad8e

https://qiita.com/takeryo_dayo/items/30431ef95bf7c38642a7

https://book.amethyst.rs/stable/

http://blog.livedoor.jp/mizuki_izuna/archives/24883864.html

Discussion