指针事件的冒泡与相机控制器
事件冒泡:父级也能接住子级的事件
Pointer<E> 实现了 Bevy 的 EntityEvent trait,带有 auto_propagate 标记——事件会自动从目标实体向父级传播。这意味着:你在父级节点挂一个 Observer,就能接住所有子节点的指针事件。
Listing 25-5 演示了冒泡:一个父实体(ShapeGroup)下挂了三个子 Mesh,父级的 Observer 能接住任何一个子 Mesh 的点击:
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, MeshPickingPlugin))
.add_systems(Startup, setup)
.run();
}
#[derive(Component)]
struct ShapeGroup;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
PointLight {
contact_shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
commands
.spawn((
ShapeGroup,
Transform::default(),
Visibility::default(),
))
.observe(|click: On<Pointer<Click>>| {
info!("group caught click from {:?}", click.entity);
})
.with_children(|parent| {
parent.spawn((
Mesh3d(meshes.add(Cuboid::new(0.8, 0.8, 0.8))),
MeshMaterial3d(materials.add(Color::srgb(0.2, 0.7, 0.9))),
Transform::from_xyz(-1.5, 0.0, 0.0),
));
parent.spawn((
Mesh3d(meshes.add(Sphere::new(0.5))),
MeshMaterial3d(materials.add(Color::srgb(0.9, 0.3, 0.3))),
Transform::from_xyz(0.0, 0.0, 0.0),
));
parent.spawn((
Mesh3d(meshes.add(Torus::new(0.3, 0.6))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
Transform::from_xyz(1.5, 0.0, 0.0),
));
});
}Listing 25-5:事件冒泡——父级 Observer 接住子级的 Click 事件
cargo run -p ch25-picking --example listing-25-05点任意一个子物体,控制台输出类似:
INFO ch25_picking::listing_25_05: group caught click from Entity(5v1#4294967301)click.entity 始终是最初被点击的子实体,而不是父实体。事件冒泡只是让父级的 Observer 也能触发,不会改变事件的目标。
控制冒泡
在 Observer 里调用 event.propagate(false) 可以阻止事件继续向上传播。这在需要"事件只由某一层处理"时很有用——比如 UI 面板拦截点击,不让它穿透到后面的 3D 物体。
冒泡的遍历路径
Pointer<E> 的冒泡路径由 PointerTraversal 决定:先走到父级(通过 ChildOf 关系),如果没有父级就走到窗口实体。这保证了事件不会无缘无故丢失。
FreeCamera:WASD 飞行控制
到目前为止,相机都是固定在一个位置的。在 3D 编辑器、调试工具或探索型游戏里,你通常需要一个可以自由飞行的相机——WASD 移动、鼠标转向。
Bevy 在 bevy_camera_controller crate 里提供了 FreeCamera 组件,但它是 feature-gated 的,需要在 Cargo.toml 里开启:
bevy = { version = "=0.18.1", features = ["free_camera"] }Listing 25-6 展示了 FreeCamera 的基本用法:
use bevy::{
camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
prelude::*,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(FreeCameraPlugin)
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 1.0, 5.0).looking_to(Vec3::NEG_Z, Vec3::Y),
FreeCamera {
walk_speed: 5.0,
run_speed: 15.0,
sensitivity: 0.2,
..default()
},
));
commands.spawn((
PointLight {
contact_shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
let floor = meshes.add(Plane3d::default().mesh().size(20.0, 20.0));
let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
let white = materials.add(Color::WHITE);
let blue = materials.add(Color::srgb(0.2, 0.7, 0.9));
commands.spawn((
Mesh3d(floor),
MeshMaterial3d(white),
));
for i in -2..=2 {
commands.spawn((
Mesh3d(cube.clone()),
MeshMaterial3d(blue.clone()),
Transform::from_xyz(i as f32 * 2.0, 0.5, 0.0),
));
}
info!("Click to grab mouse, WASD to move, QE for up/down, Shift to run, M to toggle grab");
}Listing 25-6:FreeCamera 控制器——WASD 飞行,鼠标转向
cargo run -p ch25-picking --example listing-25-06两步接入:
- 加插件——
FreeCameraPlugin注册了读取输入、更新相机位置的系统; - 挂组件——
FreeCamera挂在相机实体上,配置按键绑定和速度参数。
FreeCamera 的默认键位:
| 按键 | 动作 |
|---|---|
| 鼠标移动 | 转向 |
| 左键按住 | 捕获鼠标 |
| M | 切换鼠标捕获 |
| W / S | 前进 / 后退 |
| A / D | 左移 / 右移 |
| E / Q | 上升 / 下降 |
| Shift | 加速 |
| 滚轮 | 调整移动速度 |
FreeCamera 的字段
FreeCamera 存储的是静态配置——运行时不变的参数(除非你手动改):
sensitivity: f32——鼠标灵敏度(默认 0.2)walk_speed: f32——普通移动速度(默认 5.0)run_speed: f32——加速移动速度(默认 15.0)scroll_factor: f32——滚轮调速的倍率friction: f32——松手后的减速摩擦力key_forward/back/left/right/up/down: KeyCode——按键绑定
运行时状态(当前速度、朝向等)存在 FreeCameraState 组件里,它通过 #[require(FreeCameraState)] 自动添加,不需要手动管理。
与 Picking 的配合
FreeCamera 默认用左键捕获鼠标。这意味着如果你同时用了 Picking 和 FreeCamera,左键既会触发 Click 事件,也会触发鼠标捕获。通常的做法是:
- 拖拽物体时,相机不捕获鼠标(鼠标需要自由移动)
- 右键或特定按键控制相机转向
Listing 25-7 把 Picking 和 FreeCamera 组合在一起,展示两者如何共存。