Skip to content

给游戏加调试层

Gizmos 画的是"你想看到的线";线框渲染(wireframe)画的是"模型本身的三角面结构"。两者互补:Gizmos 可视化逻辑(碰撞体、路径、朝向),线框可视化拓扑(面数、布线、法线方向)。

线框渲染:WireframePlugin

Bevy 的线框渲染由 WireframePlugin 提供。它需要 GPU 支持 POLYGON_MODE_LINE 特性——这意味着它只在原生平台(DX12、Vulkan、Metal)上工作,WebGL/WebGPU 不支持。

rust
WireframePlugin::default(),

配置全局线框通过 WireframeConfig 资源:

rust
.insert_resource(WireframeConfig {
            global: true,
            default_color: WHITE.into(),
            ..default()
        })

global: true 让所有 Mesh 都显示线框。default_color 设定全局线框颜色。

逐实体控制

线框可以在实体级别精细控制:

组件效果
Wireframe强制显示线框,无视全局设置
NoWireframe强制不显示线框,无视全局设置
WireframeColor { color }覆盖该实体的线框颜色
rust
// 这个方块永远没有线框
commands.spawn((
    Mesh3d(meshes.add(Cuboid::default())),
    NoWireframe,
));

// 这个方块永远有线框,颜色是 AQUA
commands.spawn((
    Mesh3d(meshes.add(Cuboid::default())),
    Wireframe,
    WireframeColor { color: AQUA.into() },
));

运行时也可以切换全局线框:

rust
fn toggle_wireframe(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut config: ResMut<WireframeConfig>,
) {
    if keyboard.just_pressed(KeyCode::KeyG) {
        config.global = !config.global;
    }
}

完整示例

rust
use bevy::color::palettes::css::*;
use bevy::pbr::wireframe::{NoWireframe, Wireframe, WireframeColor, WireframeConfig, WireframePlugin};
use bevy::prelude::*;
use bevy::render::render_resource::WgpuFeatures;
use bevy::render::settings::{RenderCreation, WgpuSettings};
use bevy::render::RenderPlugin;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins.set(RenderPlugin {
                render_creation: RenderCreation::Automatic(Box::new(WgpuSettings {
                    features: WgpuFeatures::POLYGON_MODE_LINE,
                    ..default()
                })),
                ..default()
            }),
            // ANCHOR: wireframe_plugin
            WireframePlugin::default(),
            // ANCHOR_END: wireframe_plugin
        ))
        // ANCHOR: wireframe_config
        .insert_resource(WireframeConfig {
            global: true,
            default_color: WHITE.into(),
            ..default()
        })
        // ANCHOR_END: wireframe_config
        .add_systems(Startup, setup)
        .add_systems(Update, toggle_wireframe)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // 红色方块:NoWireframe,永远不显示线框
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(Color::from(RED))),
        Transform::from_xyz(-1.5, 0.5, 0.),
        NoWireframe,
    ));

    // 绿色方块:跟随全局设置
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(Color::from(LIME))),
        Transform::from_xyz(0., 0.5, 0.),
    ));

    // 蓝色方块:始终线框,自定义颜色
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(Color::from(BLUE))),
        Transform::from_xyz(1.5, 0.5, 0.),
        Wireframe,
        WireframeColor { color: AQUA.into() },
    ));

    // 地面
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(6., 6.))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
    ));

    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2., 2.5, 5.).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    commands.spawn((PointLight::default(), Transform::from_xyz(2., 4., 2.)));

    commands.spawn((
        Text::new("按 G 切换全局线框"),
        Node {
            position_type: PositionType::Absolute,
            top: px(12.),
            left: px(12.),
            ..default()
        },
    ));
}

// ANCHOR: toggle
fn toggle_wireframe(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut config: ResMut<WireframeConfig>,
) {
    if keyboard.just_pressed(KeyCode::KeyG) {
        config.global = !config.global;
    }
}
// ANCHOR_END: toggle

Listing 27-5:线框渲染——全局开关与逐实体控制

运行:

console
cargo run -p ch27-devtools --example listing-27-05

三个方块排成一排:红色的永远无线框(NoWireframe),绿色的跟随全局设置,蓝色的永远有线框且颜色是青色(WireframeColor)。按 G 切换全局线框——绿色方块的线框会出现/消失,红蓝两块不受影响。

综合调试层

实际项目中,你会同时用到 Gizmos、FPS Overlay 和线框。Listing 27-6 展示一个"调试层"系统:用 Gizmos 为每个物体画包围盒和朝向,叠加 FPS 监控。

rust
fn debug_overlay(mut gizmos: Gizmos, query: Query<&Transform>, time: Res<Time>) {
    let t = time.elapsed_secs();

    for transform in &query {
        let pos = transform.translation;
        if pos.y > 0. {
            gizmos.cube(
                Transform::from_translation(pos).with_scale(Vec3::splat(0.82)),
                Color::srgb(0.9, 0.9, 0.1),
            );
            let forward = transform.forward().as_vec3();
            gizmos.ray(pos + Vec3::Y * 0.5, forward * 0.6, Color::srgb(0.2, 0.8, 0.2));
        }
    }

    let scan_x = ops::sin(t) * 5.;
    gizmos.line(
        Vec3::new(scan_x, 0.01, -6.),
        Vec3::new(scan_x, 0.01, 6.),
        Color::srgb(0.1, 0.8, 0.8),
    );

    gizmos.grid(
        Quat::from_rotation_x(PI / 2.),
        UVec2::splat(12),
        Vec2::splat(1.),
        Color::srgb(0.15, 0.15, 0.15),
    );
}

这个系统做了三件事:

  1. 遍历所有带 Transform 的实体,为 Y > 0 的物体画黄色包围盒和绿色朝向射线
  2. 画一条水平扫描线随时间左右移动
  3. 铺一层淡灰色网格作为地面参考

这些 Gizmos 只在你有 Gizmos 系统参数的系统里才画——发布时只要把系统从 Schedule 里移除(或者用 #[cfg(debug_assertions)] 条件编译),就一行痕迹都不留。

完整示例

rust
use bevy::dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin};
use bevy::prelude::*;
use std::f32::consts::PI;

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            FpsOverlayPlugin {
                config: FpsOverlayConfig {
                    text_config: TextFont {
                        font_size: FontSize::Px(28.0),
                        ..default()
                    },
                    text_color: Color::srgb(0.0, 1.0, 0.0),
                    ..default()
                },
            },
        ))
        .add_systems(Startup, setup)
        .add_systems(Update, debug_overlay)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0., 3., 8.).looking_at(Vec3::ZERO, Vec3::Y),
    ));
    commands.spawn((PointLight::default(), Transform::from_xyz(4., 8., 4.)));

    for i in -2..=2 {
        commands.spawn((
            Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))),
            MeshMaterial3d(materials.add(Color::srgb(0.6, 0.6, 0.7))),
            Transform::from_xyz(i as f32 * 2., 0.4, 0.),
        ));
    }

    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(12., 12.))),
        MeshMaterial3d(materials.add(Color::srgb(0.25, 0.35, 0.25))),
    ));
}

// ANCHOR: debug_system
fn debug_overlay(mut gizmos: Gizmos, query: Query<&Transform>, time: Res<Time>) {
    let t = time.elapsed_secs();

    for transform in &query {
        let pos = transform.translation;
        if pos.y > 0. {
            gizmos.cube(
                Transform::from_translation(pos).with_scale(Vec3::splat(0.82)),
                Color::srgb(0.9, 0.9, 0.1),
            );
            let forward = transform.forward().as_vec3();
            gizmos.ray(pos + Vec3::Y * 0.5, forward * 0.6, Color::srgb(0.2, 0.8, 0.2));
        }
    }

    let scan_x = ops::sin(t) * 5.;
    gizmos.line(
        Vec3::new(scan_x, 0.01, -6.),
        Vec3::new(scan_x, 0.01, 6.),
        Color::srgb(0.1, 0.8, 0.8),
    );

    gizmos.grid(
        Quat::from_rotation_x(PI / 2.),
        UVec2::splat(12),
        Vec2::splat(1.),
        Color::srgb(0.15, 0.15, 0.15),
    );
}
// ANCHOR_END: debug_system

Listing 27-6:综合调试层——Gizmos + FPS Overlay

运行:

console
cargo run -p ch27-devtools

一排灰色立方体铺在绿色地面上,每个都被黄色包围盒框住、头部伸出绿色朝向箭头,一条青色扫描线来回扫过,左上角是绿色 FPS 数字。这就是一个最小的"开发模式"画面。