Picking 系统架构
Bevy 的 Picking 不是一个组件、一个系统,而是一套分层框架。理解它的三层结构,后面的所有用法都是自然推论。
三层分工
- 输入层(
bevy_input→bevy_picking::input)——把鼠标/触摸的硬件事件翻译成统一的PointerInput,写入事件队列; - 后端层(backends)——每个后端负责一种"命中检测":
bevy_ui检测 UI 节点,bevy_sprite检测 2D 精灵,MeshPickingPlugin检测 3D 网格。后端往PointerHits里写"指针碰到了哪些实体、距离多远"; - 事件层(
bevy_picking::events)——把PointerHits翻译成高层事件:Over、Out、Click、Drag……这些事件以Pointer<E>的形式发出,支持 Observer 监听和事件冒泡。
鼠标点击
│
▼
PointerInput(输入层)
│
▼
PointerHits(后端层:UI / Sprite / Mesh 各写各的)
│
▼
Pointer<Click>(事件层:统一格式,自动冒泡)
│
▼
你的 Observer 闭包MeshPickingPlugin:不在 DefaultPlugins 里
这是新手最常踩的坑:DefaultPlugins 包含了 UI 和 Sprite 的 picking 后端,但不包含 3D 网格的。原因是 3D 射线检测开销较大,而且很多游戏会用自己的物理引擎做射线。要用 3D picking,必须手动加:
App::new()
.add_plugins((DefaultPlugins, MeshPickingPlugin))加了之后,所有带 Mesh3d 的实体默认都能被鼠标点到——不需要额外组件。不想被点到的实体加 Pickable::IGNORE。
Pointer<E>:统一的事件容器
所有指针事件都包裹在 Pointer<E> 里:
pub struct Pointer<E: Debug + Clone + Reflect> {
pub entity: Entity, // 事件目标实体
pub pointer_id: PointerId, // 哪根指针(鼠标/触摸)
pub pointer_location: Location,
pub event: E, // 具体事件数据
}E 可以是 Click、Over、Out、Press、Release、Drag、DragStart、DragEnd、DragDrop……每种 E 携带不同的字段。Pointer<E> 实现了 Deref<Target = E>,所以可以直接读 drag.delta,不用写 drag.event.delta。
Observer 模式:.observe()
Picking 事件走 Bevy 的 Observer 机制(第 8 章介绍过)。用法是在实体上链式调用 .observe():
commands
.spawn((Mesh3d(...), MeshMaterial3d(...)))
.observe(|click: On<Pointer<Click>>| {
info!("clicked on {:?}", click.entity);
});Observer 闭包的第一个参数是 On<Pointer<E>>,可以通过 .entity 拿到目标实体,通过 .event(或 Deref)拿到事件数据。闭包还可以带额外的系统参数(Query、Res 等),跟普通系统一样。
事件冒泡
Pointer<E> 标注了 #[entity_event(propagate = PointerTraversal, auto_propagate)]——事件会自动从目标实体向父级传播,直到某个 Observer 调用 event.propagate(false) 阻止。这意味着:你在父级节点挂一个 Observer,就能接住所有子节点的事件,不需要逐个挂。
下一节开始写代码:先用 MeshPickingPlugin + Click Observer 做一个最小可用的点选 demo。