拖拽 3D 物体
拖拽是 Picking 系统最常见的用法之一。Bevy 的 Drag 事件已经帮你处理了"按下→移动→松开"的状态机,你只需要在 Observer 里读 delta 并更新位置。
基本拖拽:屏幕像素到世界坐标
Listing 25-3 把立方体绑定到 Drag 事件,让它跟着鼠标在 XZ 平面上移动:
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 物体移动
cargo run -p ch25-picking --example listing-25-03关键只有一行:
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——从拖拽起点到当前位置的总距离(像素)
大多数拖拽逻辑只需要 delta。distance 适合做"拖了多远才触发"的阈值判断。
地面的 Pickable::IGNORE
Listing 25-3 给地面加了 Pickable::IGNORE。这很重要——如果不加,地面也会响应 Drag 事件,鼠标在地面上拖动时会产生两套 Drag 事件(一套给立方体、一套给地面),互相干扰。
拖放:在容器之间搬运物体
更复杂的交互是拖放(Drag and Drop):把一个物体从 A 拖到 B,松手时落到 B 上。Bevy 的 Picking 系统为这种场景提供了完整的事件链:DragEnter → DragOver → DragDrop(或 DragLeave)。
Listing 25-4 演示了 3D 拖放:一个球可以在两个"接收区"(平面)之间拖来拖去:
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 事件——在两个区域之间拖放球体
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 里,被拖的球没有子级,所以不需要关心冒泡。
下一节看事件冒泡的更多用法。