Skip to content

指针事件与 Observer 集成

Picking 框架把鼠标/触摸的原始输入翻译成一组语义清晰的高层事件。每种事件对应一个 Rust 结构体,包裹在 Pointer<E> 里。

事件族谱

事件触发时机关键字段
Over指针进入实体边界hit: HitData
Out指针离开实体边界hit: HitData
Press指针按下(在实体上)button, hit
Release指针抬起button, hit
Click按下 + 抬起(在同一个实体上)button, hit, duration
Move指针在实体上移动hit, delta
DragStart按下后开始移动button, hit
Drag拖拽持续中button, distance, delta
DragEnd拖拽结束(松手)button, distance
DragEnter拖拽的物体进入另一个实体button, dragged, hit
DragOver拖拽的物体在另一个实体上方移动button, dragged, hit
DragLeave拖拽的物体离开另一个实体button, dragged, hit
DragDrop拖拽的物体放到另一个实体上button, dropped, hit

事件按顺序触发:一个完整的拖放流程是 DragStartDrag(多次)→ DragEnd;如果拖到了另一个实体上,还会触发 DragEnterDragOver(多次)→ DragLeaveDragDrop

悬停效果:Over/Out 切换材质

Listing 25-2 用 Over/Out 事件切换立方体的颜色——鼠标悬停时变黄,移开时恢复:

rust
use bevy::prelude::*;

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

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let default_matl = materials.add(Color::srgb(0.2, 0.7, 0.9));
    let hover_matl = materials.add(Color::srgb(0.9, 0.7, 0.2));

    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((
            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
            MeshMaterial3d(default_matl.clone()),
        ))
        .observe(
            move |over: On<Pointer<Over>>, mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(over.entity) {
                    mat.0 = hover_matl.clone();
                }
            },
        )
        .observe(
            move |out: On<Pointer<Out>>, mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(out.entity) {
                    mat.0 = default_matl.clone();
                }
            },
        );
}

Listing 25-2:Over/Out 事件驱动的悬停材质切换

console
cargo run -p ch25-picking --example listing-25-02

两个 Observer 闭包结构完全对称:Over 换成悬停材质,Out 换回默认材质。闭包的第一个参数类型决定了它响应哪种事件——Rust 的类型系统帮你做了事件路由。

注意闭包用了 move 来捕获材质句柄。Handle<StandardMaterial> 实现了 Clone,克隆只是引用计数加一,不复制数据。

通用 Observer 工厂

如果有很多实体需要同样的悬停效果,可以写一个泛型函数来生成 Observer:

rust
fn update_material_on<E: EntityEvent>(
    new_material: Handle<StandardMaterial>,
) -> impl Fn(On<E>, Query<&mut MeshMaterial3d<StandardMaterial>>) {
    move |event, mut query| {
        if let Ok(mut material) = query.get_mut(event.event_target()) {
            material.0 = new_material.clone();
        }
    }
}

用的时候:

rust
commands
    .spawn((...))
    .observe(update_material_on::<Pointer<Over>>(hover_matl.clone()))
    .observe(update_material_on::<Pointer<Out>>(default_matl.clone()));

EntityEvent trait 提供了 event_target() 方法,返回事件的目标实体 ID——跟直接访问 .entity 等价,但在泛型上下文里更方便。

HitData:命中信息

每种事件携带的 HitData 包含:

  • position: Option<Vec3>——命中点的世界坐标(Mesh picking 才有)
  • normal: Option<Vec3>——命中面的法线方向
  • distance: f32——命中点到相机的距离

这些信息在做精确交互时很有用——比如在命中点放置一个标记,或者根据法线方向决定物体的朝向。Listing 25-2 只用了实体 ID 来切换材质,没用到 HitData;后面的拖拽和拖放会用到 position