输入样本:坐标必须携带它所属的 Viewport 世代

自动关联目录:输入样本:坐标必须携带它所属的 Viewport 世代

核心判断:指针坐标不是两个浮点数,而是“在哪个 Viewport、哪一代尺寸和安全区规则下测得”的不可变事实。丢掉上下文后,再精确的坐标也可能命中错误对象。

一、业务冲突:同一次触摸要穿过三套坐标系

创作模式的场景工具、运行时 UI 和脚本都要消费鼠标或触摸。桌面端有 Slate 屏幕坐标,移动端有设备安全区,游戏内容又使用 Viewport 与 UI 根节点坐标;窗口缩放、横竖屏和安全区变化还可能发生在一次手势中间。

工程真正遇到的难点不是坐标换算公式,而是事件排队后 Viewport 已经变化。若 Move 用新尺寸重算、Begin 却来自旧尺寸,同一手势会产生跳变;若本地多人下拿不到唯一 LocalPlayer,却随便选择第一个,输入会稳定地路由给错误用户。

二、实际实现:不可变样本冻结身份、坐标和修订号

工程把平台事件先适配成不可变样本,同时保存 Render、Viewport、Screen、UI Safe Area 坐标,以及 Viewport 身份、Viewport Generation、Inset Revision、指针来源、PointerId、按钮、修饰键、单调序号和时间。路由阶段只消费该样本,不再次读取当前窗口状态。

适配层还记录原始指针身份,因为 Slate 在 TouchEnded 前后可能清除“由触摸模拟鼠标”的标志。长帧恢复会按指针保留关键转折与最新点,但只合并坐标域、Viewport 世代、安全区修订和修饰键均相同的样本,并限制每帧恢复预算。

flowchart LR
    P[平台/Slate 事件] --> A[适配并冻结]
    A --> S[不可变 Pointer Sample]
    V[Viewport 身份/世代] --> S
    I[安全区修订] --> S
    S --> R[路由与捕获]
    R --> T[工具]
    R --> U[UI]
    R --> G[脚本/玩法]

三、值得保留:失败关闭和同域压缩

无法唯一解析 LocalPlayer 或 Viewport 时拒绝路由,而不是回退到“第一个玩家”,这是正确的失败策略。指针样本带完整坐标来源,让录制回放、命中诊断和跨帧恢复都有证据,不必猜测事件到达时窗口是什么状态。

长帧下不盲目回放全部 Move,也不只留下最后一点;在同一坐标域内保留最远变化与最终位置,能在有界成本下维持拖拽轨迹。Begin/End 不参与任意丢弃,保证手势拓扑完整。

四、结构性缺陷:物理键仍直接泄漏到业务语义

当前归一化重点覆盖键鼠与触摸,游戏手柄在脚本输入路径前被过滤;按键别名固化在代码中,也未形成统一的死区、曲线、长按、重复和改键契约。这样“按下某键”仍会穿透到业务层,桌面功能可以工作,却无法证明控制器和触摸表达的是同一动作。

压缩策略本身也必须声明语义。绘制轨迹、签名或精细路径编辑不能使用“最远点加末点”的近似;相机旋转和拖拽物体可以。若恢复算法没有按 Action 标注允许的误差,它会从性能保护悄悄变成输入数据损坏。

五、目标设计:Sample、Control 与 Action 三层分离

第一层 InputSample 只描述设备事实与坐标域;第二层 ControlState 做控制器死区、触摸手势、重复和轴曲线;第三层 SemanticAction 才表达移动、确认、镜头旋转或编辑选择。改键配置映射 Control 到 Action,不改变玩法代码。

每个 Action 声明 ValueType、触发条件、可用 Context、是否允许样本压缩及最大误差。Context Stack 由编辑器、模态 UI、载具或角色状态持有令牌,输入系统只根据栈顶可见规则求解,不靠各模块自行判断。

ActionEvent Normalize(const InputSample& sample, const InputProfile& profile)
{
    Check(sample.viewportGeneration, sample.insetRevision);
    ControlValue v = profile.Process(sample.device, sample.control,
                                     sample.value); // dead zone / curve / gesture
    return profile.MapToAction(v, ActiveContexts());
}

六、决策与验证:先证明语义等价,再追求设备覆盖

检查项正确口径失败信号
坐标正确性命中使用样本自带的 Viewport 与 Inset 修订缩放窗口后手势跳点
本地用户归属LocalPlayer 与 Viewport 必须唯一分屏输入串号
设备等价不同设备产出同一 Action Schema业务代码判断具体按键
模拟轴处理死区与曲线属于 Profile每个角色各写一套阈值
长帧恢复仅对声明可压缩的 Action 合并绘制轨迹被削平
可诊断性Sample、Control、Action 共用 TraceId只能看到最终回调

输入架构的边界应停在“产生稳定语义动作”。它不决定角色如何移动,也不决定 UI 做什么;但必须保证每个消费者看到的是同一用户、同一坐标域、同一设备策略下的事实。