Bevy学習
※心が折れたので勉強は中止。今後公式の機能や情報や環境が充実してきたらまたやるかも。
BevyはRustベースのゲームエンジン。
Windows/Mac/Linux/Web/iOS/Androidのマルチプラットフォーム。
2D/3D両対応。
オープンソース、商用・非商用・売上に関わらず無料。
無料だがスポンサーが多数ついてるのでしばらくは開発継続されそう。
VScodeとターミナルだけで完結する。
IDEが必要か不要かは完全に人の好み。個人的にはIDE無い方が好きなのでBevyには期待している
キーボードとマウスを行ったり来たりするのって煩わしくない??
RustではないがGodotもオープンソースゲームエンジンとして名前が上がるけど、IDEが好きじゃないんだよなあ
他にもArete Engineというのもあるが、こちらはあまりにも最近できたばかりでまだまだ発展途上の印象。以前あった公式サイトもなんか消えてるし将来も不安。 ホットリロードによりソースの内容が即座に反映される。…らしいけど今のところ毎回終了してコンパイルしてる
まだベータ版。(2024年02月時点)3ヶ月に1回のペースで定期的にアップデートされるらしい。
セットアップ
code:dos
cd bevy
git checkout latest
cargo run --example breakout <-- サンプルを走らせる
開発環境
インストーラーの実行。
依存関係ライブラリのインストール(ランタイムなど)
rustがすでにセットアップされているなら、rustup updateして最新版にする。
Visual Studio Code(以下VSCode)をセットアップ。
プロジェクトの作成
code:dos
cargo new my_bevy_game <-- rust標準の空のプロジェクトを作成(Hello world)
cd my_bevy_game
cargo run <-- rustが正しくインストールされていれば Hello world!と表示される。
cargo add bevy <-- プロジェクトにBevyを追加
コンパイル最適化
以下はよくわからんがコンパイルが早くなるらしい。
code:yaml
# 注: ゲームをリリースする前に、必ずこれを元に戻してください。
# bevy = "0.12" <-- 本来はこちら
# Enable a small amount of optimization in debug mode
opt-level = 1
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
opt-level = 3
# Nightly Rust Compiler : 最新のパフォーマンス向上と「不安定な」最適化へのアクセスを提供します。
channel = "nightly"
Windows : 最新のCargo-binutilsを使用していることを確認してください。これにより、コマンドなどでcargo runLLDリンカーが自動的に使用されるようになります。
code:cmd
cargo install -f cargo-binutils
rustup component add llvm-tools-preview
ここで一旦 cargo runしてエンジンを再構築しておく。
ECSの概念
Entity Component Systemの略。プログラムをEntitiy, Component, Systemの3つにわけで考えようという設計思想。
Entity ... データ(定数)。画像や音楽やマップなど、実行中に読み込んで利用されるもの。
Component ... 構造体。実行中に随時書き換わるもの。位置情報やフラグやステータスなどをキャラクターなどの適当な単位でまとめたもの。
System ... 関数など、実際の処理。
add_systems
bevy_app::app::App
pub fn add_systems<M>(&mut self, schedule: impl ScheduleLabel, systems: impl IntoSystemConfigs<M>) -> &mut Self
このアプリのスケジュールにシステムを追加する。
schedule: どのタイミングで実行するか。Startup, Update など
systems: 実行される関数。
注意:同じscheduleの中では処理は並列に実行されるため、どの順番で結果が得られるかはわからない。順序を確定させたい場合はchainを使う。
基本的に描画処理とキー入力処理は並列で実行させると良きっぽい
Examples
app.add_systems(Update, (system_a, system_b, system_c));
app.add_systems(Update, (system_a, system_b).run_if(should_run));
chain
bevy_ecs::schedule::config::IntoSystemConfigs
pub fn chain(self) -> SystemConfigs
このコレクションを一連のシステムとして扱う。連続する要素間には順序制約が適用される。
デフォルトプラグイン(ウィンドウを作成)
DefaultPluginsをAppに追加するとウィンドウが表示される。
code:rust
fn main() {
App::new()
.add_plugins(DefaultPlugins) <-- ウィンドウが表示される
.add_systems(Startup, <セットアップ関数>) <-- 1回のみ実行される
.add_systems(Update, ( <-- 画面更新のたびに実行される
<並列処理関数名1>, <-- 単純に並べた処理は並列で実行される
(<順次処理関数名1>, <順次処理関数名2>, ...).chain(), <-- chainでつなぐと順次実行される
...
))
.run(); <-- 実行
}
プラグインの作成
プラグインは処理のまとまり。再利用できる(らしい)
インターフェイスをかけばプラグインになる
code:rust
pub struct <プラグイン名>
impl Plugin for <プラグイン名> {
fn build(&self, app: &mut App) {
app.add_systems(...
...
}
}
fn main() {
App::new()
.add_plugins((DefaultPlugins, HelloPlugin))
...
.run();
}
setupでやること
基本的にはcommand.spawnでバンドル(Bundle)をシーンに置いていく。
commandはmut commands: Commandsで取得。
バンドルはエンティティ(たとえばスプライトの形状や位置など)の情報をひとまとめにしたもの。
各バンドルは、静的な Component タイプのセットを表します。バンドルは各 Component を1つだけ含むことができ、これが満たされないと初期化時にパニックになります。
Res<...>はリソースを指していて、ローカルファイル、キー入力などを取得できる(update内でも使える)
Assetフォルダ内のファイルはRes<AssetServer>で参照できる
code:rust
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default()); //シーン内にカメラ設置
commands.spawn((SpriteBundle {...}, Direction::Up)); //スプライトと移動方向をバンドルして登録
}
スプライトの表示(表示のみ)
code:rust
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default()); <-- spawnでシーン内に設置(カメラ)
commands.spawn( <-- spawnでシーン内に設置(スプライト)
SpriteBundle { <-- SpriteBundleでスプライト定義
texture: asset_server.load("mario.png"), <-- asset_server.loadで画像読み込み
..default() <-- 最後のdefaultはおまじない?
}
);
}
SpriteBundle
code:rust
pub struct SpriteBundle {
pub sprite: Sprite,
pub transform: Transform, //位置、回転、拡大率
pub global_transform: GlobalTransform, //(readonly)計算された実際の位置、回転、拡大率
pub texture: Handle<Image>, //パターン定義
pub visibility: Visibility, //可視性
pub inherited_visibility: InheritedVisibility, //(readonly)継承された可視性(親が非表示なら非表示かも)
pub view_visibility: ViewVisibility, //(readonly)表示状態だけど描画の必要あるかしら
}
Transform
位置、回転、拡大率を管理する。
Transform::from_xyz(<x座標>: f32, <y座標>: f32, <z座標>: f32): XYZ座標を決定
他にlooking_at()looking_to()など
code:rust
pub struct Transform {
pub translation: Vec3,
pub rotation: Quat,
pub scale: Vec3,
}
Updateでやること
画面の更新
基本的にはsetupでspawnさせた時点で画面内に描画され続ける。
キャラクターの移動
キー入力
Query<B>はシーン内にspawn済のエンティティからこのバンドル(B)を持つエンティティを処理対象にする、らしい
バンドルの一部だけ適合しても対象にならない
スプライトの移動
Res<Time>.delta_seconds()は最後の更新(フレーム)からの経過時間を返します。
FPSは不定であることを念頭に置くこと。数値を足したり引いたりするとき、必ずこの値をかけるようにしないと、フレームドロップしたときに入力値が足りなくなる。
例として座標移動のとき x += A * time.delta_seconds()のときのAは「1秒で移動したい距離」と等しくなる。
でもキビキビしたアクションゲームを作りたい時、画面を左から右へ横断するのに1秒かけるとしても1920以上…とやたら桁がデカくなるので、加算時に適度に倍したほうが良さそうな気がする
code:rust
fn sprite_movement(time: Res<Time>, mut sprite_position: Query<(&mut Transform, &mut CharaState)>) {
for (mut transform, mut state) in &mut sprite_position {
//加速度の反映
transform.translation.x += state.dx * time.delta_seconds() * 100.0;
transform.translation.y += state.dy * time.delta_seconds() * 100.0;
//減速
state.dx *= 0.75 * time.delta_seconds();
state.dy *= 0.75 * time.delta_seconds();
if state.dx < 0.1 {
state.dx = 0.0;
}
if state.dy < 0.1 {
state.dy = 0.0;
}
//移動範囲制限
if transform.translation.x > 320. {
transform.translation.x = 320.;
state.dx = 0.
} else if transform.translation.x < -320. {
transform.translation.x = -320.;
state.dx = 0.
}
if transform.translation.y > 320. {
transform.translation.y = 320.;
state.dy = 0.
} else if transform.translation.y < -320. {
transform.translation.y = -320.;
state.dy = 0.
}
}
}
キー入力
Res<Input<KeyCode>>.pressed(KeyCode::<キーコード>)`
code:rust:
fn update_config(mut config: ResMut<GizmoConfig>, keyboard: Res<Input<KeyCode>>, time: Res<Time>) {
if keyboard.pressed(KeyCode::Right) {
config.line_width += 5. * time.delta_seconds();
}
if keyboard.pressed(KeyCode::Left) {
config.line_width -= 5. * time.delta_seconds();
}
}