Bevy 0.20 迁移指南:cursor 光标模块从 bevy_feathers 迁入 bevy_picking
在 Bevy 当前仓库(版本 0.20.0-dev)中,bevy_feathers 的 cursor 模块已被整体迁移到 bevy_picking 之下,custom_cursor 特性也随之转移。阅读本文后,你将掌握这次迁移涉及的类型、导入路径与特性开关的具体变化,能够正确修改自己的 use 语句与 Cargo.toml,并理解 EntityCursor、DefaultCursor、OverrideCursor 与 CursorIconPlugin 在新位置的实现原理:即如何根据指针悬停实体自动切换窗口鼠标光标,以及 custom_cursor 特性如何逐层打通 bevy_window 与 bevy_winit。
迁移内容总览
根据迁移说明文档 _release-content/migration-guides/cursor_module_to_bevy_picking.md,本次迁移(对应上游 PR #25294)包含两部分变化:
bevy_feathers中的cursor模块——包含EntityCursor、DefaultCursor、OverrideCursor和CursorIconPlugin四个公开类型——从bevy_feathers::cursor移动到bevy_picking::cursor;custom_cursor特性从bevy_feathers迁移到bevy_picking。
最直接的改动就是导入路径:
// Before(0.20 之前)
use bevy_feathers::cursor::{CursorIconPlugin, DefaultCursor, EntityCursor, OverrideCursor};
// After(当前仓库版本)
use bevy_picking::cursor::{CursorIconPlugin, DefaultCursor, EntityCursor, OverrideCursor};
模块本身在 crates/bevy_picking/src/lib.rs 中以 pub mod cursor; 的形式公开(见第 160 行),实现代码位于 crates/bevy_picking/src/cursor.rs。
四个类型的职责与实现
DefaultCursor:无悬停时的回退光标
DefaultCursor 是一个资源(Resource),指定当鼠标没有悬停在任何带光标设置的实体上时,窗口使用的默认光标图标。其实现是一个对 EntityCursor 的透明包装:
/// A resource that specifies the cursor icon to be used when the mouse is not hovering over
/// any other entity.
#[derive(Deref, Resource, Debug, Clone, Default, Reflect)]
#[reflect(Resource, Debug, Default)]
pub struct DefaultCursor(pub EntityCursor);
它支持 Deref,因此 Res<DefaultCursor> 可以直接按 EntityCursor 使用;同时派生了 Reflect,可参与 ECS 反射体系。
EntityCursor:悬停实体的光标形状
EntityCursor 是组件(Component),插入到实体上后,当指针悬停该实体时,窗口光标会被设置为该值。它是一个枚举:
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq, FromTemplate)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
pub enum EntityCursor {
#[cfg(feature = "custom_cursor")]
/// Custom cursor image.
Custom(CustomCursor),
#[default]
/// System provided cursor icon.
System(SystemCursorIcon),
}
EntityCursor::Custom(CustomCursor):仅在custom_cursor特性开启时存在,允许使用自定义光标图片(CustomCursor来自 bevy_window);EntityCursor::System(SystemCursorIcon):使用系统提供的标准光标(箭头、等待、文本框等),且是该枚举的#[default]。
该类型还提供两个转换方法(见 cursor.rs 第 54-75 行):
to_cursor_icon():把EntityCursor转为bevy_window::CursorIcon,以便插入窗口实体;eq_cursor_icon():比较当前值与窗口已有的CursorIcon是否一致,用于避免每帧重复写入窗口组件。源码中的注释特别解释了它的实现动机:当bevy_feathers未启用custom_cursor时,无法静态判断bevy_window侧是否启用了该特性,因此借助cursor_icon.as_system()包装函数,让bevy_window自行按自身特性决定比较逻辑,从而在所有特性组合下都能编译通过且无不可达分支。
OverrideCursor:全局覆盖
OverrideCursor 是另一个资源,内部为 Option<EntityCursor>:
/// A resource used to override any [`EntityCursor`] cursor changes.
/// This is meant for cases like loading where you don't want the cursor to imply
/// you can interact with something.
#[derive(Deref, Resource, Debug, Clone, Default, Reflect)]
pub struct OverrideCursor(pub Option<EntityCursor>);
它的用途是全局压过任何实体级的 EntityCursor——典型场景是加载期间强制显示等待光标,避免误导用户以为可以交互。
CursorIconPlugin 与 update_cursor 系统
CursorIconPlugin 是入口插件,其 build 方法做了两件事(见 cursor.rs 第 124-131 行):
- 若资源尚未初始化,则
init_resource::<DefaultCursor>()和init_resource::<OverrideCursor>(); - 把
update_cursor系统注册到PreUpdate调度,并放入PickingSystems::Last系统集。
update_cursor 系统的决策逻辑是理解整条链路的钥匙:
let cursor = r_override_cursor.0.as_ref().unwrap_or_else(|| {
hover_map
.and_then(|hover_map| match hover_map.get(&PointerId::Mouse))
...
.unwrap_or(&r_default_cursor)
});
优先级为:
OverrideCursor有值时,直接使用它;- 否则查询
HoverMap中鼠标(PointerId::Mouse)悬停的实体集合,逐一查找带EntityCursor组件的实体(排除Window实体),并沿ChildOf父链向上回溯(parent_query.iter_ancestors),子实体未设置时继承祖先的光标; - 都找不到时回落到
DefaultCursor资源。
确定目标光标后,系统遍历所有带 Window 组件的实体,若窗口当前的 CursorIcon 与新值不相等(eq_cursor_icon 判断),才执行 commands.entity(entity).insert(cursor.to_cursor_icon()),天然支持多窗口场景并避免无谓写入。
值得注意的运行时机:系统挂在 PickingSystems::Last,即 PreUpdate 中所有 picking 系统集(ProcessInput → Backend → Hover → PostHover → Last,定义于 lib.rs 第 259-277 行)之后——此时 HoverMap 已由本帧的悬停计算更新完毕,光标切换始终基于最新悬停状态。
custom_cursor 特性的迁移路径
特性迁移在 Cargo 层面的证据链如下:
- crates/bevy_picking/Cargo.toml:
custom_cursor = ["bevy_window/custom_cursor"](第 13 行),即bevy_picking的该特性现在直接转发给bevy_window; - crates/bevy_internal/Cargo.toml:统一入口
bevy的custom_cursor特性(第 404-407 行)展开为三个依赖项特性:
custom_cursor = [
"bevy_window/custom_cursor",
"bevy_winit/custom_cursor",
"bevy_picking/custom_cursor",
]
- crates/bevy_feathers/Cargo.toml:
bevy_feathers对bevy_picking的依赖已固定启用custom_cursor特性(第 26-28 行),因此使用 feathers 的EntityCursor::Custom变体无需用户再额外声明特性。
从源码结构看,这条特性链最终落到 crates/bevy_winit/src/cursor/mod.rs:bevy_winit 在 custom_cursor 开启时才会编译自定义光标模块、维护 WinitCustomCursorCache 光标缓存,并在渲染循环中通过 event_loop.create_custom_cursor(cursor) 创建 winit 层的自定义光标。也就是说,光标的“决策”(Bevy 侧)与“落地”(winit 侧)被特性开关严格对齐。
bevy_feathers 自身也已完成内部切换:crates/bevy_feathers/src/lib.rs 第 30 行改为 use bevy_picking::cursor::{CursorIconPlugin, DefaultCursor, EntityCursor};,FeathersCorePlugin::build 中直接注册 CursorIconPlugin(第 79 行),并插入默认值 DefaultCursor(EntityCursor::System(SystemCursorIcon::Default))(第 98-100 行)。由于旧路径 bevy_feathers::cursor 已删除,第三方 crate 若仍按旧路径导入将直接编译失败,必须按前文 “After” 代码更新导入。
实际用例验证
仓库内的示例 examples/ui/widgets/feathers_gallery.rs 展示了迁移后 API 的典型用法:通过 picking::cursor::{EntityCursor, OverrideCursor} 导入类型,并在加载中把覆盖光标设为系统等待光标:
Some(EntityCursor::System(SystemCursorIcon::Wait))
配合 OverrideCursor 资源即可实现“加载时禁用交互暗示”的效果,与源码文档注释中的设计意图一致。
迁移检查清单
针对升级 0.20.0-dev 的项目,建议按以下顺序核对:
- 全局替换导入路径:把
bevy_feathers::cursor::{...}替换为bevy_picking::cursor::{...},涉及EntityCursor、DefaultCursor、OverrideCursor、CursorIconPlugin四个类型; - 更新特性声明:如果
Cargo.toml中对bevy或bevy_feathers启用了custom_cursor,确认改为/补充启用bevy_picking/custom_cursor(或直接依赖统一的bevy入口特性,由 bevy_internal 转发); - 依赖可达性:直接使用
bevy_picking::cursor需要项目依赖bevy_pickingcrate(或经由bevy入口与bevy_picking特性启用,见 crates/bevy_internal/Cargo.toml 第 352 行bevy_picking = ["dep:bevy_picking"]); - 行为不变确认:迁移只改变了模块归属与特性位置,
update_cursor的优先级逻辑(Override → 悬停实体/祖先链 → Default)、多窗口写入与变化检测行为均保持原样。
适用前提说明:以上结论均基于当前仓库 0.20.0-dev 版本源码与迁移文档;EntityCursor::Custom 变体仅在 custom_cursor 特性开启时存在,未启用该特性的代码应只使用 EntityCursor::System 分支。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00