🐣

🦀Rust🦀 bevy:3次元の基礎

2023/11/17に公開

bevy

Rustによるゲーム開発を試したければ、真っ先に目に入るのはbevyです。bevyは現時点でちゃんとしたゲームエンジンに一番近い状態にあるとされています。しかも、Unity風コンポーネントシステムではなくECSアーキテクチャを使用します。

ECS

オブジェクト指向とは異なり、関連する状態と関数をすべて同じ"オブジェクト"にまとめたりはしません。E=エンティティ(ID的なもの)に、C=コンポーネント(状態)とS=システム(関数)を加えることで、そのエンティティの挙動を変えることができます。

システムとはなんなのかより明白にします。多数のボールが引力の影響を受けるように、PositionとRigidBodyコンポーネントを持っているとします。次に、applyForce()システムを使い、フレームごとに引力の影響を計算し、ボールの位置を更新することができます。つまり、同じコンポーネントを持っているエンティティに適用できる関数をシステムと言います。

また、メソッドをエンティティ外に定義することで同じ連続したメモリ領域に入れるエンティティの数は大幅に増加します。すると、メインメモリからデータを取り出し、キャッシュに書き込む頻度は大幅に減少します。ここで、メインメモリへのアクセスはキャッシュの約50倍以上時間がかかることを思い出してください。

では、早速bevyで簡単な3次元シーンを作りましょう。

シーンを準備

bevyプロジェクトのセットアップは公式サイトをご参照いただければと思います。

典型的なbevyプログラムはAppの定義から始まります:

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

だが、ことの時点でゲームを実行しても、真っ黒な画面しか現れません。そこに、スタートアップシステムを渡しましょう:

fn main() {
    App::new()
	.add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

まずadd_plugins()でデフォルトのプラグインを追加します。これがないとシーンにオブジェクトを入れることはできません。
次に、下のsetup()関数でシーンを準備します。setup()は開発者が指定するもので、まずカメラを配置します:

fn setup(mut commands: Commands) {
    // カメラを追加
    commands.spawn(Camera3dBundle {
        transform: Transform::from_xyz(0.0, 6., 12.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
        ..default()
    });
}

つまり、commands.spawn()はUnityのInstantiate()のように、ゲームオブジェクトの生成に用いられます。ここでクイズですが、commands.spawn()に渡されるのECSのどれですか?

エンティティです。そしてともに渡されるtransformはこのエンティティのコンポーネントですね。

さて、カメラを追加したが、シーンに光源はまだありませんから、以前として画面は真っ黒です。同じsetup()関数に次のコマンドを追加しましょう:

// 光を追加
commands.spawn(PointLightBundle {
    point_light: PointLight {
        intensity: 9000.0,
        range: 100.,
        shadows_enabled: true,
        ..default()
    },
    transform: Transform::from_xyz(8.0, 16.0, 8.0),
    ..default()
});

シーンは少しだけ灰色っぽくなってきました、素晴らしい!

球の追加

まずはボールを空中に表示します。そのためにメッシュを使いますが、まずsetupの引数にmeshesを渡す必要があります:

fn setup(..., mut meshes: ResMut<Assets<Mesh>>) {
    ...
    let sphere = meshes.add(shape::UVSphere::default().into());

    commands.spawn(PbrBundle {
        mesh: sphere,
        // このxyzはカメラの向きと同じ
        transform: Transform::from_xyz(0.0, 1.0, 0.0),
        ..default()
    });
}

球には確かに表示されますが、ピンク色になっています。日焼けした日の丸みたいですね。

bevyの公式例を参考に、テクスチャを指定します:

fn uv_debug_texture() -> Image {
    const TEXTURE_SIZE: usize = 8;

    let mut palette: [u8; 32] = [
        255, 102, 159, 255, 255, 159, 102, 255, 236, 255, 102, 255, 121, 255, 102, 255, 102, 255,
        198, 255, 102, 198, 255, 255, 121, 102, 255, 255, 236, 102, 255, 255,
    ];

    let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
    for y in 0..TEXTURE_SIZE {
        let offset = TEXTURE_SIZE * y * 4;
        texture_data[offset..(offset + TEXTURE_SIZE * 4)].copy_from_slice(&palette);
        palette.rotate_right(4);
    }

    Image::new_fill(
        Extent3d {
            width: TEXTURE_SIZE as u32,
            height: TEXTURE_SIZE as u32,
            depth_or_array_layers: 1,
        },
        TextureDimension::D2,
        &texture_data,
        TextureFormat::Rgba8UnormSrgb,
    )
}

球にテクスチャを適用します:

let debug_material = materials.add(StandardMaterial {
    base_color_texture: Some(images.add(uv_debug_texture())),
    ..default()
});

...
PbrBundle {
    mesh: sphere,
    material: debug_material.clone(), // テクスチャを渡す
    transform: Transform::from_xyz(0.0, 1.0, 0.0),
    ..default()
},

これで球が見えてきました。最後に、DefaultPluginsにImagePlugin::defaul_nearest()を渡すことで、色が正しく表示されます。

App::new()
        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
	...

落ちろ

球が見えてきましたが、他に何も起こらずあまり面白くはありません。このシーンを少し楽しくするために、球をとりあえず落下させましょう。
このために物理エンジンが必要になりますが、bevyには独自の物理エンジンがありません。

「bevy physics engine」を検索すればbevy rapierやbevy xpbdが出てくると思いますので、今回は後者を採用します。元のファイルに戻ると:

use bevy_xpbd_3d::prelude::*;

fn main() {
    App::new()
        .add_plugins(PhysicsPlugins::default())
        ...
}

これでプラグインを追加しました。あとは球にRigidBody::Dynamicコンポーネントをつければ、ちゃんと落下してくれるはずです。

// タプルを使って複数のコンポーネントが指定できる
commands.spawn((
  RigidBody::Dynamic,
  PbrBundle {
    mesh: sphere,
    material: debug_material.clone(),
    transform: Transform::from_xyz(0.0, 1.0, 0.0),
    ..default()
  },
));

しかしこれでも落下しません。コンソールを見れば、警告も表示されます:

Dynamic rigid body 1v0 has no mass or inertia. This can cause NaN values. Consider adding a `MassPropertiesBundle` or a `Collider` with mass.

つまり、RigidBodyだけで重量属性はまだありませんので、MassPropertiesBundleかColliderが必要なわけです。あとで必要になるからColliderを使います。

commands.spawn((
  RigidBody::Dynamic,
  Collider::ball(1.0), // 1.0はColliderの半径
  PbrBundle {
    mesh: sphere,
    material: debug_material.clone(),
    transform: Transform::from_xyz(0.0, 1.0, 0.0),
    ..default()
  },
));

今回はちゃんと落下してくれます。ただ、ゆかがありませんおでこのまま視野から消えてしまいます。床も準備しましょう:

commands.spawn((
  RigidBody::Static,
  Collider::cuboid(side_len, 0.002, side_len),
  PbrBundle {
    mesh: meshes.add(
	shape::Plane::from_size(side_len)
	    .into(),
    ),
    material: materials.add(Color::SILVER.into()),
    transform: Transform::from_xyz(0., -1.5, 0.),
    ..default()
  },
));

今回は床にRigidBodyとColliderコンポーネントを加えたのは球がただ床を透き通らないようにするためです。あとは、RigidBody::Staticでないと床も落下しますのでご注意願います。

おまけ:ゲームエディタ

公式なbevyエディタはまだありません。代わりにbevy editor plsが使えます。Unity、Godot、UE5などのエディタには程遠いですが、基本的な機能を備えています。追加は他のプラグインと同じ:

use bevy_editor_pls::prelude::*;

fn main() {
    App::new()
        .add_plugins(EditorPlugin::default())
        ...
}

ゲームを実行すれば、画面の左上に「Open window」の隣にポーズボタンが現れ、クリックするとエディタが展開されます。シーンのオブジェクトのプロパティを確認・変更するためのメニューのほか、ゲームビューを右クリックしながら動かせば角度の変更も、マウスのスクロールでズームもできます。

まとめ

これでbevyエンジンの基本の紹介は完了となります。もちろん、今回構築したシーンは非常にシンプルなのでbevyの機能のごく一部にしか触れませんでした。他の機能を使い、より複雑なオブジェクトを生成することや、かっこいいレンダーシステムを試していきましょう。

Discussion