RustのゲームエンジンAmethystを触れてみる

[PONOS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ponos)の10日目の記事です。
昨日は、 @e73ryo さんの [Unityエディタの中を見て理解を深める](https://qiita.com/e73ryo/items/e67a5838eea79cc7facd) でした。

## はじめに

近年で最も注目されているであろうプログラミング言語Rustを触れてきました。過去の記事では2019年、2020年のアドベントカレンダーでRustとWebSocketを使用してリアルタイム通信を実装した記事を記述しております。なぜRustが注目されているか理由を簡単に記述しますと「とにかく実装速度が速い」「ゼロコスト抽象化の追求」「モダンな言語機能が入っている」「安全性が高い」「ツールが充実」しているなどなどが挙げられるかと思います。
それら特徴を聞いただけで魅力的なRust言語ですが、バックエンド開発以外にもゲームの開発ができるのかということに興味が湧きました。調べてみるとRust製のゲームエンジンは「Piston」「Amethyst」「bevy」などが存在しますが、今回はECSアーキテクチャを採用「Amethyst」を触れてみようかと考えました。

## Amethyst

今回触れてみるRust製のゲームエンジン「Amethyst」の最大の特徴はECSアーキテクチャを採用してる点なのかなと考えております。ECSはエンティティ・コンポーネント・システムの略語であり、Entity（エンティティ）、Component（コンポーネント）、System（システム）という大きな3つの役割からなる設計思想になります。

Entityは、ゲーム内のオブジェクトの識別子であり、キャラクターやステージの障害物などゲーム内に存在するオブジェクトそれぞれを表、具体的な機能は持ちません。
Componentは、機能を分解したデータであり、位置やスケール、画像や3Dモデルの描画機能などを構成します。EntityはそのComponentが紐づけられています。また、Componentは配列的に管理されます。同一の機能を配列的に管理することで、機能のデータの管理をメモリ的に連続した配置で管理できます。
Systemは、Componentへの作用を行います。Component配列に対して一括で処理を行い、ゲーム内のロジックを実現します。

補足ですが、ECSアーキテクチャはコンポーネント指向のUnityエンジンにも実装されていますので、取っ付きやすい方が多いのではないかというのもこのエンジンを触れてみる理由の一つになっております。

## 実装
RustでAmethystを使用してWindowを表示するだけの実装を実施したいと思います。実装環境としてはローカル(macOS)で実施することを前提しています。

|開発環境|バージョン|
|:-:|:-:|
|macOS|11.15.2|
|Rust(rustc、cargo)|1.52.1|
|Amethyst|0.15.3|

### 事前準備
開発端末にRustを導入しなくてはいけませんが、導入する方法は[2019年アドベントカレンダーの17日目の記事](https://qiita.com/nisei275/items/a60cb3d3e97aeb429fa7)で記載しておりますので、今回は省略します。

1. amethyst_toolsのインストール

    ```
    $ cargo install amethyst_tools
    ```

1. プロジェクトの作成

    ```
    $ amethyst new [プロジェクト名]
    ```

プロジェクトを作成すると以下のようなディレクトリ構成が生成されます。
今回、srcディレクトリには`lib.rb`、`state.rs`、stateディレクトリの中に`game.rs`を手動で作成しています。

```
├──Cargo.toml
├──README.md
├──assets         // 画像データなどを格納するディレクトリ
├──config
│  └──display.ron // ウィンドウの設定
└──src
   ├──main.rs
   ├──lib.rs      // 追加ライブラリソース
   ├──state.rs    // 追加ステートソース
   └──state
      └──game.rs  // 追加ゲームステートソース
```
### 実装する
各種追加したコードを記述します。

```toml:Cargo.toml
[package]
name = "project name"
version = "0.1.0"
authors = ["XXXXX <XXXXX@email.com>"]
edition = "2018"

[dependencies]
amethyst = "0.15.3"

[features]
default = ["vulkan"]
empty = ["amethyst/empty"]
metal = ["amethyst/metal"]
vulkan = ["amethyst/vulkan"]
```

```ron:display.ron
(
    title: "2D Action Game",
    dimensions: Some((1280, 720)),
)
```

```rust:main.rs
use amethyst::{
    prelude::*,
    utils::application_root_dir,
    renderer::{
        plugins::{RenderFlat2D, RenderToWindow},
        types::DefaultBackend,
        RenderingBundle
    }
};

use [Project Name]::state::game::GameState;

fn main() -> amethyst::Result<()> {
    // amethystエンジンのログを開始
    amethyst::start_logger(Default::default());

    // 設定ファイルおよびアセットのパスを読み込む。
    let app_root = application_root_dir()?;
    let assets_dir = app_root.join("assets");
    let config_dir = app_root.join("config");
    let display_config_path = config_dir.join("display.ron");

    // RenderingBundleの定義
    let rendering_bundle = RenderingBundle::<DefaultBackend>::new()
                            .with_plugin(
                                // ウィンドウを開いてレンダリングターゲットを表示する。
                                // display_config_pathを指定すると、WindowBundleを使用してウィンドウを構成
                                RenderToWindow::from_config_path(display_config_path)?
                                    .with_clear([0.0, 0.0, 0.0, 1.0]),
                            )
                            // フラットシェーディング(均一の輝度で描画)で2Dオブジェクトを描画
                            .with_plugin(RenderFlat2D::default());

    // ゲームデータ用のビルダーをデフォルトで作成しwith_bundleで様々なBundleを読み込む。
    let game_data = GameDataBuilder::default()
        .with_bundle(rendering_bundle)?;

    // 指定された初期ゲーム状態で新しいアプリケーションを作成し実行する。
    // アセットのパス、初期ステート、ゲームデータを指定する。
    let mut game = Application::new(assets_dir, GameState, game_data)?;
    game.run();

    Ok(())
}
```
```rust:lib.rs
pub mod state;
```
```rust:state.rs
pub mod game;
```
```rust:state/game.rs
use amethyst::{
    input::is_key_down,
    prelude::*,
    winit::VirtualKeyCode,
};

// Defaultトレイトを継承(new()が使用可能)
#[derive(Default)]
pub struct GameState;

// ゲームステート
// SimpleStateは単純な状態特性。StateDataとしてGameDataが含まれ、カスタムStateEventは含まれていません。
impl SimpleState for GameState {
    // イベントへの対応に使用するために、更新前に毎フレームで実行される。
    fn handle_event(&mut self, _state_data: StateData<'_, GameData<'_, '_>>, event: StateEvent) -> SimpleTrans {
        // winit windowによって送信されたイベントがある場合に処理する。
        if let StateEvent::Window(event) = event {
            // エスケープキーが押下されたらアプリケーションを停止させる。
            if is_key_down(&event, VirtualKeyCode::Escape) {
                Trans::Quit
            } else {
                Trans::None
            }
        } else {
            Trans::None
        }
    }
}
```

## 実行
実装で記述したソースコードを元に以下コマンドでコンパイルおよび実行します。
今回、グラフィックスAPIはvulkanを使用します。そのためMacOSで実行する場合は、[Vulkan](https://vulkan.lunarg.com/sdk/home)からSDKをダウンロードする必要があります。今回このAPIを使用しているのはWindowsやLinuxでも使用できるマルチプラットフォームであるためそれを採用しましたが、導入手間を省きたい場合は、MacOSの場合、Metalを指定します。ただし、`Cargo.toml`の`default = ["vulkan"]`を`metal`に変更してください。

```
$ cargo run --features vulkan // vulkanを使用してビルド&実行
$ cargo run --features metal  // metalを使用してビルド&実行
```

※ なぜ、グラフィックAPIを変更する際に`default = ["vulkan"]`の値を変更しないといけないのかというと、値を変更したないで`metal`で実行した場合は以下エラーが発生しました。

```
unresolved import `amethyst::renderer::types::DefaultBackend`
```

以下、Githubのコメントで英語を翻訳してみると、defaultとは別のグラフィックAPIを指定することはできないようです。
https://github.com/amethyst/amethyst/issues/2284

### 実行結果
以下のようなウィンドウが表示されたら成功になります。
![Screen Shot 2021-12-07 at 15.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191987/4487fa25-97c3-2228-a234-1c13e71d96e7.png)

# まとめ
RustのゲームエンジンAmethystをWindowが出るところまで実施してみました。今までRustはバックエンドで触れてきましたが、ゲーム開発も行えることを確認できました。今はUnityやUnreal Engineなど優れたゲームエンジンがある中でRustを使用したゲーム開発は敷居が高いものとなりますが、昔のようにプログラミング言語のみでゲームの開発を勉強してみたい方達にとってはいい教材になりうるのではないかと思います。
次回は、Amethyst関連の記事を作成する際は、ゲームスプライトを設置して操作するような実装をする予定です。

明日は @caramel_cafe さんになります。お楽しみに！

## 参考文献
* Rust+ECSでゲーム開発 -ゲームエンジンAmethystのススメ- / 住田 直樹
  <br>https://www.amazon.co.jp/dp/4295600059