Skip to content

拖拽 3D 物体

拖拽是 Picking 系统最常见的用法之一。Bevy 的 Drag 事件已经帮你处理了"按下→移动→松开"的状态机,你只需要在 Observer 里读 delta 并更新位置。

基本拖拽:屏幕像素到世界坐标

Listing 25-3 把立方体绑定到 Drag 事件,让它跟着鼠标在 XZ 平面上移动:

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>>,
) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 8.0, 12.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(Plane3d::default().mesh().size(20.0, 20.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))),
        Pickable::IGNORE,
    ));

    commands
        .spawn((
            Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
            MeshMaterial3d(materials.add(Color::srgb(0.2, 0.7, 0.9))),
        ))
        .observe(on_drag_move);
}

fn on_drag_move(drag: On<Pointer<Drag>>, mut transforms: Query<&mut Transform>) {
    if let Ok(mut transform) = transforms.get_mut(drag.entity) {
        transform.translation.x += drag.delta.x * 0.01;
        transform.translation.z += drag.delta.y * 0.01;
    }
}

Listing 25-3:Drag 事件驱动的 3D 物体移动

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

关键只有一行:

rust
transform.translation.x += drag.delta.x * 0.01;
transform.translation.z += drag.delta.y * 0.01;

drag.delta屏幕像素的增量(从上一帧到这一帧鼠标移了多少像素),不是世界坐标。delta.x 对应水平方向(加到 translation.x),delta.y 对应垂直方向——但屏幕的 Y 轴向下,而世界坐标的 Z 轴也向"远处",所以 delta.y 加到 translation.z 上。系数 0.01 是经验性的缩放,让拖拽手感匹配鼠标移动。

Drag 事件的字段

Drag 结构体有两个向量:

  • delta: Vec2——本帧的增量(像素)
  • distance: Vec2——从拖拽起点到当前位置的总距离(像素)

大多数拖拽逻辑只需要 deltadistance 适合做"拖了多远才触发"的阈值判断。

地面的 Pickable::IGNORE

Listing 25-3 给地面加了 Pickable::IGNORE。这很重要——如果不加,地面也会响应 Drag 事件,鼠标在地面上拖动时会产生两套 Drag 事件(一套给立方体、一套给地面),互相干扰。

拖放:在容器之间搬运物体

更复杂的交互是拖放(Drag and Drop):把一个物体从 A 拖到 B,松手时落到 B 上。Bevy 的 Picking 系统为这种场景提供了完整的事件链:DragEnterDragOverDragDrop(或 DragLeave)。

Listing 25-4 演示了 3D 拖放:一个球可以在两个"接收区"(平面)之间拖来拖去:

rust
use bevy::prelude::*;

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

#[derive(Component)]
struct DropZone;

#[derive(Component)]
struct Draggable;

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 8.0, 12.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    commands.spawn((
        PointLight {
            contact_shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));

    let zone_matl = materials.add(Color::srgb(0.25, 0.45, 0.25));
    let zone_hover_matl = materials.add(Color::srgb(0.35, 0.6, 0.35));

    commands
        .spawn((
            DropZone,
            Mesh3d(meshes.add(Plane3d::default().mesh().size(4.0, 4.0))),
            MeshMaterial3d(zone_matl.clone()),
            Transform::from_xyz(-3.0, 0.01, 0.0),
        ))
        .observe(
            move |enter: On<Pointer<DragEnter>>,
                  mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(enter.entity) {
                    mat.0 = zone_hover_matl.clone();
                }
            },
        )
        .observe(
            move |leave: On<Pointer<DragLeave>>,
                  mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(leave.entity) {
                    mat.0 = zone_matl.clone();
                }
            },
        )
        .observe(|drop: On<Pointer<DragDrop>>, mut transforms: Query<&mut Transform>| {
            if let Ok(mut transform) = transforms.get_mut(drop.dropped) {
                transform.translation = Vec3::new(-3.0, 0.5, 0.0);
            }
        });

    let zone2_matl = materials.add(Color::srgb(0.25, 0.25, 0.55));
    let zone2_hover_matl = materials.add(Color::srgb(0.35, 0.35, 0.7));

    commands
        .spawn((
            DropZone,
            Mesh3d(meshes.add(Plane3d::default().mesh().size(4.0, 4.0))),
            MeshMaterial3d(zone2_matl.clone()),
            Transform::from_xyz(3.0, 0.01, 0.0),
        ))
        .observe(
            move |enter: On<Pointer<DragEnter>>,
                  mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(enter.entity) {
                    mat.0 = zone2_hover_matl.clone();
                }
            },
        )
        .observe(
            move |leave: On<Pointer<DragLeave>>,
                  mut mats: Query<&mut MeshMaterial3d<StandardMaterial>>| {
                if let Ok(mut mat) = mats.get_mut(leave.entity) {
                    mat.0 = zone2_matl.clone();
                }
            },
        )
        .observe(|drop: On<Pointer<DragDrop>>, mut transforms: Query<&mut Transform>| {
            if let Ok(mut transform) = transforms.get_mut(drop.dropped) {
                transform.translation = Vec3::new(3.0, 0.5, 0.0);
            }
        });

    commands
        .spawn((
            Draggable,
            Mesh3d(meshes.add(Sphere::new(0.4))),
            MeshMaterial3d(materials.add(Color::srgb(0.9, 0.3, 0.3))),
            Transform::from_xyz(0.0, 0.5, 0.0),
        ))
        .observe(on_drag_move);
}

fn on_drag_move(drag: On<Pointer<Drag>>, mut transforms: Query<&mut Transform>) {
    if let Ok(mut transform) = transforms.get_mut(drag.entity) {
        transform.translation.x += drag.delta.x * 0.01;
        transform.translation.z += drag.delta.y * 0.01;
    }
}

Listing 25-4:DragDrop 事件——在两个区域之间拖放球体

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

拖放的关键事件:

  • DragEnter——拖拽的物体进入接收区。event.dragged 是被拖的实体 ID,event.entity 是接收区的实体 ID。这里用来给接收区换高亮材质;
  • DragOver——拖拽物体在接收区上方移动。持续触发,适合实时更新预览位置;
  • DragDrop——松手时触发。event.dropped 是被放下的实体。这里用来更新球的位置到接收区中心;
  • DragLeave——拖拽物体离开接收区。用来恢复接收区的默认材质。

注意观察者挂在接收区(DropZone)上,不是挂在被拖的物体上。被拖物体只需要一个 on_drag_move 处理移动;接收区需要监听 DragEnter/DragOver/DragDrop/DragLeave 四种事件来管理高亮和放置逻辑。

Drag 事件的传播

Pointer<Drag> 事件会自动冒泡。如果拖拽的物体是某个父节点的子级,父节点的 Observer 也会收到事件。可以通过 event.propagate(false) 阻止冒泡。在 Listing 25-4 里,被拖的球没有子级,所以不需要关心冒泡。

下一节看事件冒泡的更多用法。