Open11

Rust製ゲームエンジンBevyの使い方

ヘンゼルの記憶ヘンゼルの記憶

Bevy って何?

Rust製のゲームエンジン。
公式サイトは https://bevyengine.org/
現時点の最新は 0.4.0 です。

特徴

それでこのスクラップは?

ヘンゼルが Bevy の使い方を勉強しながらメモしていきます。
2Dゲームがターゲットなのでたぶん 3D でてきません。
いろいろできるようになったら本にまとめてみたいという野望ありです。

ヘンゼルの記憶ヘンゼルの記憶

準備

cargo.toml
[dependencies]
bevy = "0.4"

Windows なら基本これだけで OK です。

ただし Microsoft Visual C++ Build Tools の 2017 を使っている場合にはビルドが通らないようです(Bevy 0.3.0 link error on Windows 10)。
その場合 2019 に上げるしかなさそうです。

Linux の場合もうちょっと準備が必要です。
下記を参考にがんばりましょう!

Mac は…ほとんど触ったことないのでよくわかりませんがきっと大丈夫でしょう。

ヘンゼルの記憶ヘンゼルの記憶

基本的な使い方

超最低限

main.rs
use bevy::prelude::*;

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

何もしません。
ウィンドウすら出ません。

ウィンドウが出るだけ

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .run();
}

DefaultPluginsいろんなプラグインをまとめて追加してくれます。

なお環境によってはウィンドウの高さを変えて 0 にすると panic します(しました)。 → Decreasing window height to 0 causes crash #170

なにか表示する

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup.system())
        .run();
}

fn setup(commands: &mut Commands) {
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            sprite: Sprite::new(Vec2::new(32.0, 32.0)),
            ..Default::default()
        });
}

真ん中に 32x32 の白い正方形が表示される…だけです。

なにか表示した

ヘンゼルの記憶ヘンゼルの記憶

ウィンドウの設定

基本的な設定

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_resource(WindowDescriptor {
            title: "タイトル!".to_string(),
            width: 480.,
            height: 640.,
            resizable: false,
            ..Default::default()
        })
        // ↑↑↑DefaultPlugins より先に指定する
        .add_plugins(DefaultPlugins)
        .run();
}

設定できる項目一覧

項目 デフォルト 備考
title String "bevy".to_string()
width f32 1280.
height f32 720.
vsync bool true
resizable bool true
decorations bool true false にするとウィンドウの枠が非表示なる
cursor_visible bool true
cursor_locked bool false true にするとマウスをキャプチャしたままになる
mode WindowMode WindowMode::Windowed 他に BorderlessFullscreen、Fullscreen がある

起動後に設定を取得・変更する

ウィンドウサイズを取得しウィンドウタイトルに反映します。
ウィンドウサイズを変更するとリアルタイムで変化します。

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_system(update_title.system())
        .run();
}

fn update_title(mut windows: ResMut<Windows>) {
    if let Some(window) = windows.get_primary_mut() {
        window.set_title(
            format!("ウィンドウサイズ: {} x {}", window.width(), window.height())
        )
    }
}

背景色の設定

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_resource(ClearColor(Color::rgb(0.8, 0.5, 0.2)))
        .add_plugins(DefaultPlugins)
        .run();
}

背景色設定例

ヘンゼルの記憶ヘンゼルの記憶

スプライト

スプライトは画面に描画される絵です。
2Dゲームではこれが基本です。

デフォルトのスプライト

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup.system())
        .run();
}

fn setup(commands: &mut Commands) {
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle::default());
}

Bevy ではカメラがないと何も表示されません。
2D であればとりあえず Camera2dBundle::default() を追加すれば OK です。

…がそれだけではまだ何も表示されません。
SpriteBundle のデフォルトは白い矩形なのですがサイズが 0x0 になっているためです。

サイズを指定すれば表示されます。
コードは「基本的な使い方」に記載済みなので省略します。

単色スプライト表示

スプライトの画像を変更する場合は material を指定します。

material の型は Handle<ColorMaterial> です。
Handle の説明に

Handles contain a unique id that corresponds to a specific asset in the Assets collection.

とあります。
つまり画像を Assets に登録して得られたハンドルを material に指定する必要があります。

例えば一面同じ色を塗るだけの場合こんな感じです。

main.rs
fn setup(commands: &mut Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            sprite: Sprite::new(Vec2::new(32., 32.)),
            material: materials.add(Color::LIME_GREEN.into()),
            ..Default::default()
        });
}

Color には LIME_GREEN のような色の定数が多数定義されています。
また関数 rgb 等で任意の色を生成することもできます。

画像ファイルを使ったスプライト表示

png画像を表示したい場合はもうひと手間かかります。
AssetServer でロードした画像を Assets に登録して使います。

main.rs
fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            material: materials.add(texture.into()),
            ..Default::default()
        });
}

Sprite表示の例

load に指定するパスは絶対パス、相対パスどちらでもOKです。
相対パスの場合 assets フォルダが基準になります。
上記コードの場合 myface.png は assets フォルダに格納されています。

png 以外どのような画像フォーマットに対応しているかは未確認です。
jpeg はダメなようですが。

画像ファイルを使ったスプライトのサイズ

SpriteBundle はデフォルトで resize_mode が Automatic になっており画像を設定すると自動的にそのサイズになります。

Sprite でサイズを指定すると自動リサイズが解除されて(正確には new() で Manual が設定されて)、画像が指定サイズに合わせて拡大や縮小されます。

main.rs
fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            sprite: Sprite::new(Vec2::new(64.,256.)),
            material: materials.add(texture.into()),
            ..Default::default()
        });
}

スプライト表示の例2

ヘンゼルの記憶ヘンゼルの記憶

スプライトの移動・回転・倍率変更

transform を指定するとスプライトを表示位置を変えたり回転させたり拡大・縮小することができます。

表示位置変更

transformtranslation で表示位置を変更できます。

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup.system())
        .run();
}

fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            transform: Transform {
                translation: Vec3::new(128., 128., 0.),
                ..Default::default()
            },
            material: materials.add(texture.into()),
            ..Default::default()
        });
}

スプライトの移動

デフォルトではウィンドウの中心が (0, 0)、右上方向が + なので右上に表示されます。

スプライトの回転

transformrotation で表示する角度を指定できます。
bevy は右手座標系で、右方向がx、上方向がy、手前方向がz になっています。

なので例えば画像を右に傾けたい場合は z軸を中心にマイナス方向に回転させます。

main.rs
fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            transform: Transform {
                rotation: Quat::from_rotation_z(-45.0_f32.to_radians()),
                ..Default::default()
            },
            material: materials.add(texture.into()),
            ..Default::default()
        });
}

スプライトを右に回転

なおここで from_rotation_zfrom_rotation_xfrom_rotation_yに書き換えてx軸や y軸中心に回転させると画像が半分しか表示されなくなります。

スプライトをx軸中心で回転

Bevy ではスプライトも3Dで計算されています。
そしてスプライトのz座標はデフォルトで 0 なので x軸や、y軸を中心に回転するとスプライトの半分はz座標が負になります。
さらにz座標が負の部分は表示されない仕様なので半分しか表示されないのです。

スプライトの拡大・縮小

transformscale で表示する倍率を指定できます。
ここで負の数を指定すると画像が反転します。
なおスプライトは厚みがないので z の倍率は指定しても効果がありません。

main.rs
fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            transform: Transform {
                scale: Vec3::new(2., -0.5, 0.),
                ..Default::default()
            },
            material: materials.add(texture.into()),
            ..Default::default()
        });
}

スプライトの拡大縮小

ヘンゼルの記憶ヘンゼルの記憶

スプライトの表示順

zオーダー

z座標が大きい方が手前に表示されます。

main.rs
use bevy::prelude::*;

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup.system())
        .run();
}

fn setup(
    commands: &mut Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let texture = asset_server.load("myface.png");
    commands
        .spawn(Camera2dBundle::default())
        .spawn(SpriteBundle {
            transform: Transform {
                translation: Vec3::new(0., 0., 1.),
                ..Default::default()
            },
            material: materials.add(texture.into()),
            ..Default::default()
        })
        .spawn(SpriteBundle {
            sprite: Sprite::new(Vec2::new(64., 64.)),
            transform: Transform {
                translation: Vec3::new(32., 32., 0.),
                ..Default::default()
            },
            ..Default::default()
        });
}

zオーダー

白い矩形の z座標は 0.0、顔画像の z座標は 1.0 なので顔画像が手前に表示されます。

なお z座標が同じ場合は後に作成されたほうが手前に表示されます。
…たいていは。
保証はないらしくスプライトに対しなにか変更加えたりすると入れ替わったりするようです。

また z座標が 0 未満になったり 1000以上になると(999.9 を超えると?)表示されなくなります。

ヘンゼルの記憶ヘンゼルの記憶

2Dカメラ

Camera2dBundleTransform が含まれています。
つまりカメラに対してもスプライト同様位置(translation)や角度(rotation)、拡大率(scale)を指定できます。

カメラが表示の基準になっているためカメラを右に移動させると画面内のスプライトは全体的に左に表示されます。
カメラを時計回り(=Z軸マイナス方向)に回転させると画面内のスプライトはカメラの位置を中心に反時計回りに回転します。
さらにカメラの拡大率を 1.0 より大きくすると画面内のスプライトは逆に小さくなり位置もカメラに近づいて表示されます。

orthographic_projection はスプライトを実際の画面にどう投影するか管理しています。
デフォルトでは座標 0, 0 をウィンドウの中心に表示しますが window_originWindowOrigin::BottomLeft を指定するとウィンドウ左下を 0, 0 にすることができます。

例えばウィンドウの左下を 0, 0 にして画面全体を時計回りに 30°傾ける場合こんな感じになります。

main.rs
use bevy::{
    prelude::*,
    render::camera::{OrthographicProjection, WindowOrigin},
};

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup.system())
        .run();
}

fn setup(commands: &mut Commands) {
    commands
        .spawn(Camera2dBundle {
            orthographic_projection: OrthographicProjection {
                window_origin: WindowOrigin::BottomLeft,
                ..Default::default()
            },
            transform: Transform {
                rotation: Quat::from_rotation_z((30.0_f32).to_radians()),
                ..Default::default()
            },
            ..Default::default()
        })
        .spawn(SpriteBundle {
            sprite: Sprite::new(Vec2::new(32., 32.)),
            ..Default::default()
        });
}
ヘンゼルの記憶ヘンゼルの記憶

衝突判定

衝突判定は collide 関数を使います。
スプライトAの座標とサイズ、スプライトBの座標とサイズを渡すと衝突しているか(=重なりがあるか)判定してくれます。

さらに衝突している場合その方向も判定してくれます。
スプライトAの右側がスプライトBと重なっている場合は Some(Collision::Right) が、上側が重なっている場合には Some(Collision::Top) が返されます。
対応しているのは上下左右の4方向だけなので斜めに重なっている場合はより深く重なっている方向が返されます。

あくまで重なりを判定しているためフレーム間でスプライトが大きく移動した場合すり抜けたり衝突の方向を誤って判定してしまいます。

このような問題を回避するためにはスプライトを少しずつ動かしてそのたびに重なりを判定するか、衝突判定を自作したり手頃な別の Crate を使用する必要があります。

ヘンゼルの記憶ヘンゼルの記憶

衝突判定に使えそうな Crate

ちょっとした衝突判定なら自作したほうが早そうですが、ゲームによっては物理エンジンが欲しくなりそうですのでちょっと検索してみました。

physme

Bevy 用の物理エンジンのようですが、依存している Bevy のバージョンが 0.21 になっているので更新できていないようです。

Rapier

Bevy を公式にサポートしている物理エンジンです。
Getting Started | Rapier に説明があります。
Bevy 0.4 にも対応しているようですので現状では一番使えそうです。

Continuous Collision Detection があればよく物理エンジンまでは不要という場合下のような Crate もありますが Bevy で使いやすいかは不明です。