はじめに
本記事では、Rust製ゲームエンジンBevyを使用時に役立つデバッグ設定を紹介します。
本記事はBevyやECSの紹介・解説ではありません。Bevyについて知りたい方は、以下のリンクを参照してください。
https://bevyengine.org/learn/quick-start/introduction/
https://bevyengine.org/examples/
環境
bevy: 0.15.2
devモードを作成
開発中のみ使用したい機能がある場合、devfeatureを定義します。cfgアトリビュートを活用して、デバッグ設定を簡単にできるようにします。
[features]
default = ["dev"]
dev = [
    # release時には追加したくないfeatureをここに
    "bevy/dynamic_linking",
    "bevy/bevy_dev_tools",
]
自身のクレートのログレベルを設定
自身のクレートのみ、デバッグレベルのログを表示させます。
use bevy::prelude::*;
#[cfg(feature = "dev")]
const LOG_FILTER: &str = "wgpu=error,my_crate=debug";
#[cfg(not(feature = "dev")]
const LOG_FILTER: &str = "wgpu=error";
fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(LogPlugin {
            filter: LOG_FILTER.to_string(),
            ..default()
        }))
        .add_systems(Startup, test_debug_message)
        .run();
}
fn test_debug_message() {
    debug!("some useful debug messages");
}
bevy-inspector-eguiの導入
bevy-inspector-eguiを導入することで、Entityなどのデバッグが容易になります。また、コンポーネントの値をランタイムで調整することができるので、スタイルやカメラなどの調整にも便利。
bevy_remote_inspector
bevy-inspector-eguiのオルタナティブとして、bevy_remote_inspectorも同様の機能を持ちます。好きな方を使ってください。
各Entityに名前を付ける
Entityをスポーンする際は、できるだけNameコンポーネントを追加しましょう。
bevy-inspector-eguiでEntityリストを表示するとき、Nameコンポーネントの文字列が、ラベルとして表示されます。Nameコンポーネントを追加しないと、無名のEntityが大量に存在することになり、デバッグ時に困ります。
Entityを識別する目的でNameコンポーネントを使わないでください。Entityを識別したい場合、Markerコンポーネントパターンを使います。
spawn関数を拡張して、Nameを確実に設定する
Commandsに対する拡張トレイトとして、spawn_namedのようなメソッドを追加しても良いかもしれません。
use std::borrow::Cow;
use bevy::prelude::*;
pub trait SpawnNamedEntityExt {
    fn spawn_named<N, B>(&mut self, name: N, components: B) -> EntityCommands<'_>
    where
        N: Into<Cow<'static, str>>,
        B: Bundle;
}
impl SpawnNamedEntityExt for Commands<'_, '_> {
    fn spawn_named<N, B>(&mut self, name: N, components: B) -> EntityCommands<'_>
    where
        N: Into<Cow<'static, str>>,
        B: Bundle,
    {
        self.spawn((Name::new(name), components))
    }
}
Observerにも名前を付ける
Observerは、特定のイベントのトリガーを観測するシステムを保持するコンポーネントの一種です。add_observerを実行すると、Observerコンポーネントを持つEntityがスポーンします。
ObserverはBevy0.14で追加された比較的新しいAPIであり、将来的に破壊的変更が行われます。本記事は、Bevy0.15.2のAPIを基準に書かれています。
Observerはコンポーネントですが、その中身はTriggerを第一引数に取る関数です。したがって、Observerを持つEntityには、関数へのパスがわかる情報を追加するとデバッグ時に役立ちます。std::any::type_nameを用いて自動で名前を付けるユーティリティを書いてみました。
use bevy::{ecs::system::IntoObserverSystem, prelude::*};
use std::borrow::Cow;
pub trait AddObserverExt {
    fn add_observer_named<E: Event, B: Bundle, M>(
        &mut self,
        observer: impl IntoObserverSystem<E, B, M>,
    ) -> &mut Self;
}
impl AddObserverExt for App {
    fn add_observer_named<E: Event, B: Bundle, M>(
        &mut self,
        observer: impl IntoObserverSystem<E, B, M>,
    ) -> &mut Self {
        let name_components = observer_name_components(&observer);
        self.world_mut()
            .spawn((Observer::new(observer), name_components));
        self
    }
}
/// Observer関数の型名をもとに、デバッグ情報を生成する
fn observer_name_components<T>(observer: &T) -> impl Bundle {
    let type_name = std::any::type_name_of_val(observer);
    // エンティティ名は簡潔に
    let name = type_name.split("::").last().unwrap();
    (
        Name::new(name),
        
        // フルパスも保持しておく
        ObserverSystemTypeName(Cow::Borrowed(type_name)),
    )
}
/// Observer関数へのフルパス
#[derive(Debug, Component, Reflect)]
struct ObserverSystemTypeName(Cow<'static, str>);
メイン例:
use bevy::prelude::*;
mod utils;
use utils::*;
fn main() {
    App::new()
        .add_plugins((
            // ここではLogPluginの設定を省略しています
            DefaultPlugins,
            
            #[cfg(feature = "dev")]
            bevy_inspector_egui::quick::WorldInspectorPlugin::new(),
        ))
        .add_systems(Startup, setup)
        .add_observer_named(on_click)
        .run();
}
fn setup(mut commands: Commands) {
    commands.spawn_named("Camera2d", Camera2d);
    commands.spawn_named(
        "Interaction",
        Node {
            width: Val::Percent(100.0),
            height: Val::Percent(100.0),
            ..default()
        },
    );
}
fn on_click(
    trigger: Trigger<Pointer<Click>>,
    query: Query<(&Name, &ObserverSystemTypeName)>
) {
    let observer_entity = trigger.observer();
    let (name, path) = query.get(observer_entity).unwrap();
    debug!("name: {}", name);   // "on_click"
    debug!("path: {}", path.0); // "my_crate::on_click"
}