输入路由:Begin 被消费后,Move 和 End 不能重新判定
自动关联目录:输入路由:Begin 被消费后,Move 和 End 不能重新判定
核心判断:输入消费是一次手势的所有权协议,不是每帧重新执行的 if-else。Begin 由谁接管,后续 Move、End 或 Cancel 就必须到达同一消费者。
一、业务冲突:编辑工具、UI、脚本和角色都想拿同一个指针
一个点击可能落在场景操纵柄、运行时按钮或可交互物上;文本框获得焦点后,键盘不应继续驱动角色;应用失焦、切换模式或销毁 World 时,又必须结束所有已开始的输入。
若每个事件重新按当前命中判定,按下按钮后移出边界会把 End 发给场景,按钮永久保持 Pressed;若失焦时只是清空按键集合,消费者永远收不到释放,角色会持续移动。
二、实际实现:Begin 锁存所有权,终止阶段先 Cancel 再拆监听
工程中的路由器为每个 Source 与 PointerId 保存活动状态。Begin 先经过 GUI 阻挡、场景工具、UI 捕获和脚本动作;一旦某层消费,结果锁存到该指针,Move 与 End 不再重新仲裁。路由器用单调序号、时间校验和重入保护拒绝倒序事件,Sink 替换与 Reset 延迟到当前分发结束。
UI 自身也记录捕获对象:Begin 命中的控件继续接收 Move/End。焦点进入文本输入时会合成已按键的 End;应用失焦、Play 结束或输入管理器注销时,先向活动手势发 Cancel,再清空按键、触摸兼容状态和监听器。
stateDiagram-v2
[*] --> Idle
Idle --> Captured: Begin + winner
Captured --> Captured: Move -> same owner
Captured --> Idle: End
Captured --> Cancelling: focus lost / teardown
Cancelling --> Idle: Cancel delivered
Idle --> [*]: listener removed三、值得保留:终止事件是资源释放协议
Begin 锁存消除了“手指移动后所有权漂移”;Cancel 先于注销则让工具、UI 和脚本都有机会释放拖拽事务、按压视觉和临时资源。把失焦视为终止输入,而非简单暂停,是避免粘键最关键的设计。
文本焦点第一次接管键盘时主动 Flush 已按键,也比持续查询 Slate 焦点更可靠:消费者得到一个完整的 End 边沿,此后再由 Context 阻断新 Begin。
四、结构性缺陷:PlayerController 看门狗是在修补所有权缺失
嵌入式编辑环境会反复设置 GameAndUI、鼠标光标和点击事件,并在 Tick 中通过代理字段检测漂移后重新应用;由于 UE 的 InputMode 缺少完整 Getter,这种做法能恢复功能,却无法回答“谁有权覆盖输入模式”。全局允许应用非激活时接收 Slate 输入,也可能把后台快捷键送入游戏。
硬编码拦截 Escape、全屏和标签快捷键再转发宿主,同样把平台外壳规则塞进全局预处理器。共享纹理模式还要专门绕开回弹环路,说明输入所有权实际跨越宿主、Slate、Viewport 和玩法,却没有统一契约。
五、目标设计:用可撤销的 Input Lease 合成最终策略
编辑器、模态 UI、文本输入、相机和角色控制各申请 InputLease,声明设备范围、Action 集、优先级、消费方式、光标模式和失焦策略。聚合器生成唯一有效的 PlayerController/Enhanced Input/Slate 配置;令牌释放后重算,不由 Tick 抢写状态。
路由状态必须拥有明确终止屏障:切 World、换 Context 或替换 Sink 时先阻止新 Begin,再向旧 Owner 发送 Cancel,等待本地清理完成,最后提交新配置。宿主快捷键作为最高层 Context,也走同一套可观测决策。
void TransitionInputPolicy(InputPolicy next)
{
gate.BlockNewBegins();
for (Gesture& g : activeGestures)
g.owner->OnCancel(g.id, CancelReason::PolicyChanged);
activeGestures.Reset();
ApplyToSlateAndPlayerController(next);
gate.Open();
}六、决策与排查:查所有权链,不查最后一个布尔值
| 现象 | 首查证据 | 不应采用的修补 |
|---|---|---|
| 按钮卡在按下态 | Begin Owner 是否收到 End/Cancel | 超时强制改视觉 |
| 角色失焦后继续移动 | Flush 是否生成键盘 End | 每帧轮询按键 |
| UI 与场景同时响应 | 路由 Trace 的首个 Consumer | 两边各加阻挡布尔值 |
| 输入模式反复跳变 | 当前 Lease 集与合成结果 | Tick 里重复 SetInputMode |
| 后台触发命令 | 应用激活策略与宿主 Context | 全局吞掉所有键 |
| 切模式偶发崩溃 | Cancel、解绑与对象销毁顺序 | 回调里立即替换 Sink |
输入系统的完成条件不是“按键能触发”,而是任意生命周期出口都能证明:没有活动手势、没有悬挂按键、没有旧 Context 仍持有控制权。