【Rustのまほう2】#17 プラグイン

このひとりアドベントカレンダーでは当初はクソ真面目なことに事前に25日分の記事の計画をしていたんですが、そこまで力を入れて書いても仕方ないなーと思って、ゆるふわ無計画で書くことにしました。そのせいで、そういえばプラグインの解説を忘れていました。

Bevyにおける **プラグイン** はシステムやイベントなどの定義をひとつにまとめた構造体です。Bevyの本体はほとんど機能を持っておらず、プラグインを追加していくことで初めて様々な機能を使うことができます。



# プラグインの追加

サードパーティープラグインの機能を追加したいときは、`add_plugins`を呼ぶことでプラグインを追加できます。たとえば、私は物理エンジンに `bevy_rapier`を使っています。Bevy界隈ではもっともよく使われている物理エンジンです。

https://github.com/dimforge/bevy_rapier

これをゲームに追加して使えるようにするには、以下のように`RapierPhysicsPlugin` を追加します。またこのとき縮尺などの初期設定もできます。

```rust
app.add_plugins(
    RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(PIXELS_PER_METER)
        .in_fixed_schedule()                
        .with_custom_initialization(RapierContextInitialization::NoAutomaticRapierContext),
)
```

なお、例えば縦横`100`ピクセルの四角形に縦横`100`の剛体を仕込んでシミュレーションを実行してみると、非常にゆっくりと地面に落下します。デフォルトではRapier世界の長さ`1`は1メートルで、重力定数は`9.8`です。巨大なゴジラが緩慢な動作に見えたり、巨大な天空の城ラピュタが崩壊してゆっくり海に落下してるように見えるのと同じ理屈で、四角形は縦横100メートルの大きさの物体の落下はゆっくりに見えてしまいます。そこで `pixels_per_meter` で縮尺を変えて `1` を 1センチメートルに設定すると、`100`ピクセル四方の物体は1メートル四方の物体と同じようにポロっと落ちるようになり直感的になります。

https://rapier.rs/docs/user_guides/bevy_plugin/common_mistakes#why-is-everything-moving-in-slow-motion

物理エンジンは設定が複雑で落とし穴も多いので、ドキュメントにはよく目を通しておきましょう。簡単なゲームであれば自力で衝突判定を実装することもできます。ちなみに、私が書いた2022年のアドベントカレンダーでは、いわゆるプラットフォーマー、つまりマリオみたいな横スクロールアクションゲームを作っていますが、それでは自力で衝突判定を実装しています。

https://qiita.com/hiruberuto/items/c3a40731f44a986fdd8b

ただし、
そうやって作られたゲームは、たとえばプレイヤーキャラクターがリフトに乗ったときにリフトが動くと、プレイヤーキャラクターの位置はそのままで滑り落ちる、リフトに乗り続けるにはリフトと同じ速度で走り続けなくてはならない、みたいな奇妙なことが起きやすいです。結局は物理エンジンをちゃんと使ったほうが実装が楽だと思います。








# DefaultPlugins

初期状態ではウィンドウを開く機能やスプライトを表示する機能のようなゲーム開発の最低限の機能すらありません。このため、通常は最初に以下のように`DefaultPlugins`という **プラグイングループ** を追加して、最低限のプラグイン群を追加します。

```rust
fn main() {
    App::new().add_plugins(DefaultPlugins).run();
}
```

`DefaultPlugins`には、ウィンドウを表示するための `WindowPlugin` や、アセットを読み込むための `AssetPlugin` 、スプライトを表示するための `SpritePlugin` などが含まれます。

また、`set`を呼ぶことでこれらのプラグイングループの設定を上書きできます。たとえば、私の作っているゲームはドット絵感を強調した２Dゲームなので、カメラで拡大縮小したときも nearest neighbor で補完してドット絵のギザギザ感をわざと出すようにしています。この設定を行うには、以下のようにして `ImagePlugin` の設定を上書きします。

```rust
app.add_plugins(
    DefaultPlugins.set(ImagePlugin::default_nearest())
);
```

また、ウィンドウのタイトルやウィンドウサイズ変更の可否などは、`WindowPlugin`を上書きします。

```rust
    .set(WindowPlugin {
        primary_window: Some(Window {
            position: WindowPosition::Centered(MonitorSelection::Current),
            cursor_options: CursorOptions {
                visible: cfg!(feature = "debug"),
                ..default()
            },
            title: format!("{} 0.1", CRATE_NAME).to_string(),
            resizable: false,
            enabled_buttons: EnabledButtons {
                close: true,
                maximize: false,
                minimize: true,
            },
            focused: true,
            ..default()
        }),
        ..default()
    })
```




# プラグインの定義

プラグインは自分で定義するには、構造体を定義して`Plugin`トレイトを実装します。`build`関数を実装して、そこで`add_systems`を呼んでシステムを追加したり、`init_resource`を呼んでリソースを初期化したりします。例えば私の場合、以下のようなプラグインを定義して、カメラの初期化や更新のシステムをひとつのプラグインにまとめてあります。

```rust:camera.rs
pub struct CameraPlugin;

impl Plugin for CameraPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(OnEnter(GameState::Setup), setup_camera);
        app.add_systems(
            FixedUpdate,
            (update_camera_position, update_camera_brightness)
                .run_if(in_state(GameState::InGame))
                .before(PhysicsSet::SyncBackend),
        );
    }
}
```

実際のゲーム開発ではゲーム本体には無数のシステムが定義されますが、私は関連のあるシステムをプラグインとして整理してあります。そうしたプラグインの塊としてゲームを構成しているのですが、別にプラグインにしなくてもシステムを直接登録することもできるので、こうする必要はなかったかもしれません。