1. 现象与背景
2026-08-25,Mighty Rodent(Rust + Bevy 0.14)的 ESC 游戏内暂停菜单出现按钮交互失效问题。排查过程中,日志成为定位根因的核心工具------代码静态检查无法发现的「系统调度顺序」问题,在日志里暴露无遗。
现象如下:
- 按 ESC 呼出暂停菜单后,鼠标 hover 按钮不变黄、点击无效。
- 键盘 ↑/↓ 无法移动焦点,回车/空格无动作。
- 黄色焦点框停在按钮下方,框住空白区域。
环境:Bevy 0.14.2,macOS 15.1,窗口 640×480。
2. 第一直觉排查:方向错了
第一直觉是「按钮没挂 MenuFocusable」或「导航系统 run_if 门没包含 Paused 态」------但代码检查后都不是。这两个方向虽然常见,但在这个案例里并不成立。
真正的问题藏在系统调度顺序里,而不是组件缺失或状态门条件错误。
3. 根因:系统调度顺序
三个系统共同决定了「确认」能否被正确消费:
- 键盘导航系统
keyboard_menu_navigation_system:把焦点按钮的Interaction置为Pressed。 - 还原系统
gamepad_focus_restore_system:把Pressed改回Hovered。 - 暂停按钮系统
pause_button_system:消费Pressed,触发继续或结束。
日志里 Keyboard confirm → button 1 出现了,但 Pause button clicked 永远 0 次------说明 Pressed 在到达 pause_button_system 之前就被还原了。
这是 Bevy 0.14 系统调度顺序的经典坑:没有显式 .before() / .after() 约束时,同帧内系统执行顺序不确定。
4. 日志如何暴露问题
日志的价值在于把「系统执行顺序」变成可观测的事实。代码静态检查只能确认「每个系统单独看是对的」,但无法确认「系统之间的先后关系是否符合预期」。
通过日志可以确认:
Keyboard confirm → button 1出现,说明键盘导航系统确实执行了,焦点也确实在按钮上。Pause button clicked为 0 次,说明Pressed没有被pause_button_system消费到。- 两者结合,唯一合理的解释就是:
Pressed在到达消费系统之前,被还原系统改回了Hovered。
5. 修复方案
修复方式是在系统调度上显式声明顺序,确保 Pressed 先被消费、再被还原:
rust
app.add_systems(
Update,
(
keyboard_menu_navigation_system,
pause_button_system,
gamepad_focus_restore_system,
)
.chain()
.run_if(in_state(GameState::Paused)),
);
使用 .chain() 后,三个系统按声明顺序依次执行:键盘导航先置 Pressed,暂停按钮系统消费 Pressed,最后还原系统把 Interaction 改回 Hovered。
5.1 源码验证
为了确认根因判断,我补充了排查证据链,日志文件为 logs/run_20260825115207.log。关键证据如下:
text
# ESC 切换正常(toggle 系统在工作)
Paused
Resumed
Paused
Resumed
...(反复按 ESC)
键盘导航系统在工作(focus 移动 + confirm 触发)
Keyboard focus → button 2/2
Keyboard focus → button 1/2
Keyboard confirm → button 1
Keyboard confirm → button 2
但暂停按钮系统从未消费到 Pressed
Pause button clicked 永远 0 次
结论:键盘导航系统把 Interaction 置 Pressed 后,被还原系统抢先改回 Hovered,pause_button_system 永远收不到 Pressed。
5.2 系统调度顺序问题
src/game/mod.rs 的 GamePlugin::build 中,三个系统的注册顺序如下:
rust
// 三个系统的注册顺序(隐式顺序,不确定)
.add_systems(Update, pause_button_system)
// ... 导航系统注册在 MenuPlugin
.add_systems(Update, keyboard_menu_navigation_system)
// ... 还原系统注册在 MenuPlugin
.add_systems(Update, gamepad_focus_restore_system)
没有显式 .before() / .after() 约束时,Bevy 0.14 按注册顺序执行------但跨 plugin 的注册顺序由 plugin 添加顺序决定,不可控。
5.3 修复提交与日志验证
提交 ff7a1c4 给 pause_button_system 加显式顺序约束:
rust
.add_systems(
Update,
pause_button_system
// 确认 Pressed 必须先被暂停按钮消费
.after(crate::ui::keyboard_menu_navigation_system)
.after(crate::ui::gamepad_menu_navigation_system)
// 还原系统必须等暂停按钮消费完
.before(crate::ui::gamepad_focus_restore_system),
)
提交 47907a6 后的日志验证:
text
Keyboard focus → button 1/2
Keyboard confirm → button 1
Pause button clicked: ContinueGame ← 终于出现!
Resumed from pause
三个系统执行顺序正确:导航 → 暂停按钮(消费 Pressed)→ 还原。
6. 经验总结
这个案例的核心教训是:Bevy 0.14 中,同帧内系统执行顺序默认不确定,凡是依赖系统间先后关系的逻辑,都必须显式声明 .before() / .after() 或使用 .chain()。
日志是定位这类问题的最有效工具------它能把「系统执行顺序」从不可见的运行时行为,变成可检索、可对比、可验证的事实。
7. 落地结论
把这次排查沉淀为可复用的诊断规则,方便下次遇到类似问题直接套用。
7.1 Bevy 0.14 UI 交互诊断规则
- 规则一:按钮交互失效时,先看日志有没有「系统 A 的 log」但「系统 B 的 log」不存在------两个系统的执行顺序可能错了。
- 规则二 :跨 plugin 的系统必须显式
.before()/.after()约束,不能依赖隐式注册顺序。 - 规则三 :
Changed<Interaction>查询只在Interaction实际变化时触发------如果上游系统把Pressed改回Hovered,下游系统永远收不到Pressed。
适用范围:Bevy 0.14 所有涉及多系统协作的 UI 交互场景。
7.2 三类读者速查
人类开发者自查清单
- 日志里 confirm 触发但动作没执行 → 检查系统执行顺序,加
.before()/.after()。 - 按钮 hover 不变黄 → 检查父节点是不是
ButtonBundle,是则改NodeBundle+Pass。 - 按钮点击无效 → 检查文字子节点
FocusPolicy,加FocusPolicy::Pass。
对话式 AI
问:Bevy 0.14 键盘导航确认后动作没触发,可能原因?
答:三个系统(导航/按钮/还原)执行顺序不对------确认 Pressed 被还原系统抢先改回 Hovered,按钮系统收不到 Pressed。修复:给按钮系统加 .after(导航).before(还原)。
代码 Agent
可复制执行的诊断命令:
bash
# 1. 查日志里系统执行顺序
grep -E "Keyboard confirm|Pause button clicked" logs/run_*.log
2. 如果 confirm 有但 clicked 无 → 加顺序约束
sed -i 's/.add_systems(Update, pause_button_system)/.add_systems(Update, pause_button_system.after(keyboard_menu_navigation_system).before(gamepad_focus_restore_system))/' src/game/mod.rs
3. 验证修复后日志
./test-macos-640-480.rvs
grep "Pause button clicked" logs/run_*.log