えむおぎ GAMES

Rust製ゲームエンジン Bevy を触ってみる 1: Getting Started thumbnail

Rust製ゲームエンジン Bevy を触ってみる 1: Getting Started

はじめに

Rust とは

最近評判を上げているプログラミング言語のひとつ。 最近といっても注目され始めてから大分経ち、もはや中堅〜ベテランの顔もしている。

コードを書く時のルール (縛り) が多いのが特徴的で、ちゃんと書けば読みやすくパフォーマンスも高い。一方、 書き方を理解するまではそのメリットを活かしきれずよく分からないエラーに苦しめられたり、初心者向きではない、ともよく言われる。

個人開発で使う分には良いと思います。将来性もありそうで、勉強も兼ねて最近よく使うようにしています。

Bevy とは

https://bevy.org/

Rust 製のゲームエンジン。ECS (Entity / Component / System) というアーキテクチャを採用しているのが特徴と言われています。

Rust 製のゲームエンジンの中では一番の知名度がありそう。

Getting Started

https://bevy.org/learn/quick-start/getting-started/ を進めていきます。

まずはサンプルコードを実行します。

Rust (Cargo) はインストール済みの想定。

1
2
3
4
5
git clone https://github.com/bevyengine/bevy
cd bevy
# 現時点での最新バージョンは v0.17.2 でした。
git switch --detach latest
cargo run --example breakout

ブロック崩しが始まりました。(音も出ました)

ブロック崩し

プロジェクトの作成

サンプルが動いたところで、自分でプロジェクトを作成する方法。

これもシンプルです。

1. 普通に Rust プロジェクトを作成
1
2
cargo new my_bevy_game
cd my_bevy_game
2. Bevy を追加
1
cargo add bevy
3. ビルドの最適化設定

Cargo.toml に追記

1
2
3
4
5
6
7
# Enable a small amount of optimization in the dev profile.
[profile.dev]
opt-level = 1

# Enable a large amount of optimization in the dev profile for dependencies.
[profile.dev.package."*"]
opt-level = 3

最適化方法は他にも記載されていましたが、オプションなので今はスルー。

4. 最小構成の作成

main.rs を以下のようにする。

1
2
3
4
5
use bevy::prelude::*;

fn main() {
    App::new().run();
}

cargo run で実行。まだ何も起こらない。

コードの説明

ここからコードの説明に入ります。

最小コードに出てきた App には以下の 3 つのフィールドがあります。

world ゲームデータ全てを保管する。
schedule ゲームデータを処理するシステムと、その処理順の情報を保管する。
runner schedule に基づいて実際の挙動をコントロールする。

これらの詳細を理解するために、次に Bevy の設計思想である “ECS” の説明です。

ECS

Entity, Component, System の頭文字。それぞれが表すものは…

Entity ゲーム上のそれぞれの物。
Component Entity に割り当てるもの。
System Component を処理するのに使う。

以下のような例が挙げられている。

その次が具体例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
use bevy::prelude::*;

// Component の定義

#[derive(Component)]
struct Person;

#[derive(Component)]
struct Name(String);

// システムの定義

fn add_people(mut commands: Commands) {
    commands.spawn((Person, Name("Elaina Proctor".to_string())));
    commands.spawn((Person, Name("Renzo Hume".to_string())));
    commands.spawn((Person, Name("Zelda Hume".to_string())));
}

fn hello_world() {
    println!("hello world!");
}

fn update_people(mut query: Query<&mut Name, With<Person>>) {
    // Query を使用して Person コンポーネントを持つエンティティの Name を更新する

    for mut name in &mut query {
        if name.0 == "Elaina Proctor" {
            name.0 = "Elaina Hume".to_string();
            break;
        }
    }
}

fn greet_people(query: Query<&Name, With<Person>>) {
    for name in &query {
        println!("hello {}", name.0);
    }
}

fn main() {
    App::new()
        .add_systems(Startup, add_people)
        .add_systems(Update, (  // chain() でシステムの実行順を指定する
            hello_world,
            (
                update_people,
                greet_people
            ).chain()
        ))
        .run();
}

具体例の中身は見た通り。 関数の引数に型を指定するだけで Dependency Injection みたいに Entity を取得できるのが特徴的。

実行すると以下のようになります。

1
2
3
4
5
$ cargo run
hello world!
hello Elaina Hume
hello Renzo Hume
hello Zelda Hume

…コマンドラインばっかりで全然ゲームエンジンらしさが出てきませんね。

ゲームらしい画面を表示するため、次に Plugin の説明に入ります。

Plugin

Plugin も Bevy の特徴的な部分で、Bevy は様々な機能をプラグインとして提供しています。

UI が欲しければ UiPlugin, ゲーム画面をレンダリングしたければ RenderPlugin など。これらを入れていなかったのでコマンド画面だけだったということでした。

更に、これらのプラグインにはサードパーティーのものが多数存在します。つまり、コミュニティの人たちがそれぞれプラグインを作成し、それらを簡単に他の人も取り込める仕組みが整っているようです。

これら大量のプラグインは Bevy Assets で確認できます。

大量のプラグインがあって目が回りますが、基本的なプラグインは DefaultPlugins としてひとまとめになっているようです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)  // add
        .add_systems(Startup, add_people)
        .add_systems(Update, (
            hello_world,
            (
                update_people,
                greet_people
            ).chain()
        ))
        .run();
}

これで、空のゲーム画面が表示されるようになります。またついでに、hello world がコンソール画面を埋め尽くすようになりました。 イベントループが入り、フレームごとに hello_world が呼ばれているようです。

空のゲーム画面

ここでプラグインの作り方についての説明がありました。すぐに使うことは無さそうですが、プラグインを作りやすいのはサードパーティープラグイン開発の活発化という意味では良さそうです。

先ほど作った hello world をプラグインにする例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
pub struct HelloPlugin;

impl Plugin for HelloPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, add_people);
        app.add_systems(Update, (
            hello_world,
            (
                update_people,
                greet_people
            ).chain()
        ));
    }
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(HelloPlugin)
        .run();
}
Resource

そして “Getting Started” の最後として、Resource の説明があります。

リソースはエンティティーとコンポーネントとは違い、グローバルに単一のデータです。 例えば以下のようなものがあります。

実際に Time, Timer リソースを使い、hello world を 2 秒おきにしてみます。

リソースもコンポーネントと同じく、引数の型指定だけで取得できます。Res で read, ResMut だと write 権限と分かれています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Resource)]
struct GreetTimer(Timer);

fn greet_people(time: Res<Time>, mut timer: ResMut<GreetTimer>, query: Query<&Name, With<Person>>) {
    // update our timer with the time elapsed since the last update
    // if that caused the timer to finish, we say hello to everyone
    if timer.0.tick(time.delta()).just_finished() {
        for name in &query {
            println!("hello {}!", name.0);
        }
    }
}

impl Plugin for HelloPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(GreetTimer(Timer::from_seconds(2.0, TimerMode::Repeating)));
        app.add_systems(Startup, add_people);
        app.add_systems(Update, (update_people, greet_people).chain());
    }
}

これで greet が 2秒おきになりました。

次のステップ

これで基本 of 基本の部分が完了です。 次に読むドキュメントとして以下のようなものが挙げられています。

次は順番通り Examples か、ブラウザで動かしたいので Web Examples を見てみようと思います。

Recent Posts

Tags