交互与输入
Category
client / 横向节点 / 交互与输入。一句话:输入是玩家与权威之间的第一段链路——把异构设备事件变成稳定意图,再交给本地预测与网络上报。
Definition
输入系统只做两件事:把键盘、鼠标、触屏、手柄的原始事件归一化成意图(Intent),再把意图按时序送进本地模拟与网络层。目标函数有两个:手感(采样到上屏的延迟)与确定性(意图可复算、可重放)。它与 游戏 UI 体系 是一对反方向:UI 是「玩家读状态」的表现层,输入是「玩家写状态」的入口——UI 的手感的一半,其实诞生在输入管线的延迟账里。
Problem
- 事件散落在渲染回调里:keydown/pointer 监听直接写在 UI 组件与玩法对象上,换一种设备(触屏加手柄)就是一次全量返工——没有抽象层,每个新输入设备都是一次侵入式改造。
- 手感与权威打架:为了「跟手」把输入直接改本地状态,服务端裁决一来回滚不成立——预测能成立的前提是意图可复算(前后端边界 的口径),直接改状态抢在意图生成之前,就把这个前提拆了。
- 键位硬编码:按键语义写死在 if 分支里,改键位要发版;触屏与手柄各写一套逻辑,三个设备三份行为漂移。
- 输入时序与 tick 错位:输入发生在帧与帧之间,采样点没有定义,帧同步项目就会拿到「同一操作、不同帧号」的漂移(帧同步 的输入一致性从这里开始)。
Algorithm
管线五段
| 段 | 职责 | 关键口径 |
|---|---|---|
| 设备事件 | keydown、pointer、gamepad poll、触摸的原始采集 | 统一时间戳;轮询型设备(手柄)与事件型设备(键盘)分开处理 |
| 输入抽象 | 归一化:方向向量、按键状态、滑动轨迹 | 设备差异在这一段消化,下游不再感知设备 |
| 映射表 | context → action 的数据表(战斗态/菜单态各一张) | 键位是数据不是代码;换设备 = 换表 |
| 意图 | 带序号与时间戳的 action | 可复算、可重放、可丢弃——预测与上报都以意图为单位 |
| 消费 | 本地预测先行 + 网络上报;UI 消费与玩法消费分离 | UI 抢焦点时玩法输入要能让路(交互边界与 web/06 分层 对齐) |
手感三口径
- 采样时机固定:在 tick 边界统一采样,还是事件到达即入队再于 tick 消费——二选一,写进文档;帧同步项目只认「tick 边界出意图」。
- 延迟预算有账:采样 → 意图 → 预测上屏的整段延迟要可测量;本地渲染基准见 Canvas 2D 渲染(rAF 节拍决定输入消费的上限)。
- 回滚以意图为单位:服务端复算打回时,撤销的是意图产生的状态迁移,不是「按键本身」——预测口径见 预测、补偿与纠正。
触屏与多设备
- 虚拟摇杆是输入抽象的一个实现,不是一条新管线——输出与手柄摇杆同构,映射表不用改。
- 多指冲突按「区域所有权」裁决:一个触点只能被一个控件持有,摇杆区与按钮区的边界要在抽象层画清。
- 手柄的轮询节奏独立于渲染帧:低电量/蓝牙延迟是常态,映射表要带死区与曲线校准。
实战案例:一次「同一操作,不同设备三种判定」的输入时序漂移排查
案例为教学示例(非摘自真实项目),数字经过取整简化。它把本页「管线五段」「手感三口径」「触屏与多设备」用到深水区:同一条判定谱面在三种设备上跑出三档结果——这是时序漂移的归因排查,也是「设备差异要在源头消化」的完整展开。
背景与现象:项目同时接入键盘、触屏、手柄三种输入,同一条判定谱面的通过率键盘 96.2%、触屏 93.1%、手柄 89.8%,最大相差 6.4 个百分点;判定类工单每月 4200 条,其中 73% 在单一设备上无法稳定复现;一次「改键位」改动花了 9 人天,改完还得发版。
第一步:现象与口径——先把三种设备各自的时序账摆出来
采样口径:键盘与触屏是事件到达即入队,手柄按 120 赫兹轮询、渲染跑 60 帧——手柄平均每 2 帧才轮到一次(120 ÷ 60 = 2)。三条路径的采样点各不相同,意图到达本地模拟的时刻从一开始就不在一个平面上。
序号口径:同一 tick 内键盘连按会产生 2 个序号;取 1000 帧样本,意图序号重复 17 次——本页要求意图可复算、可重放、可丢弃,序号本身不可信,后两条就全废了。
复现口径:4200 条工单里 3066 条(73%)换设备就换结论,这类问题在单设备复现环境里永远是「随机」的。
第二步:分层归因——按本页管线五段逐段比对
输入时序归因 占比
采样时机没固定,事件与轮询两条路径并存 46%
键位硬编码,三种设备三份行为各自漂移 31%
触点区域所有权缺失,多指冲突裁不出来 23%逐项比对:手柄 89.8% 是三档里最低的一档,直接命中本页说的手柄轮询节奏独立于渲染帧、低电量与蓝牙延迟是常态;一次改键位花 9 人天还得出包,命中键位硬编码写死在 if 分支里;多指同按时摇杆与按钮互抢,命中「一个触点只能被一个控件持有」这条边界没画出来。
第三步:根因——采样时机没写进文档,设备差异被推到了下游
本页「手感三口径」第一条就是二选一:在 tick 边界统一采样,还是事件到达即入队再于 tick 消费——二选一,写进文档。项目两边都要,于是每段下游都自带一份时序行为:设备差异在这一段消化,下游不再感知设备,这句被跳过了,三个设备就各长出一条判定路径。帧同步项目只认「tick 边界出意图」,项目却连本地判定都没有统一意图出口,同一操作自然对应不同帧号。
第四步:处置与回填——把时序收敛回一个出口
- 采样统一:三种设备一律在 tick 边界出意图,事件型设备缓存到下一 tick 统一消费,手柄轮询结果对齐到最近一次 tick。
- 键位数据化:context → action 映射表按战斗态、菜单态拆两张,键位是数据不是代码,换设备 = 换表。
- 区域所有权:触点归属在输入抽象层裁决,摇杆区与按钮区边界画死,多指冲突先判所有权再判优先级。
- 序号治理:同一 tick 内意图序号只发一个,连按重复合并,回放按序号重算意图序列。
复核账:通过率三档 96.2 / 93.1 / 89.8 → 95.6 / 95.4 / 94.8(最大差 6.4 → 0.8 个百分点);判定类工单 4200 → 310 条;意图序号重复 17 → 0 次;改键位改动 9 人天 → 1 人天(换表,不发版)。
回填清单:
| 回填项 | 动作 | 回填处 |
|---|---|---|
| 采样时机 | 二选一写进文档,全部设备 tick 边界出意图 | 手感三口径 |
| 序号唯一 | 同一 tick 序号只发一个,连按重复合并 | 管线五段 |
| 键位数据化 | context → action 映射表,换设备换表 | 管线五段 |
| 区域所有权 | 触点归属在抽象层裁决,冲突先判所有权 | 触屏与多设备 |
案例的三个教训
- 采样时机不写死,时序漂移必然发生——事件与轮询两条路径并存,玩家看到的就只能是「同一操作、不同帧号」。
- 设备差异要在源头消化,不要在下游各改一遍——输入抽象层没立起来,三种设备长成三份行为,返工按设备数放大。
- 键位是数据不是代码——硬编码在 if 分支里,改键位就得发版;映射表落地后,换设备就是换表。
Used By
虚拟摇杆与手柄适配(输入抽象的设备端);MOBA/射击的键位自定义(映射表数据化);格斗与音游的输入判定(采样时机与延迟预算);帧同步 RTS 的指令时序(意图序号与 tick 边界);UI 焦点与玩法输入的抢夺治理(消费段分离)。
Related
关系链:设备事件 → 输入抽象 → 映射表 → 意图 → 消费与预测 → 时序与手感。逐段回答「为什么需要下一个」:
- 设备事件要抽象:设备差异不消化在源头,下游每一段都要重做一遍。
- 抽象了要映射:action 与按键解耦,重绑定与多设备才是数据问题。
- 映射完要出意图:预测、上报、回放消费的都是意图,不是原始事件。
- 意图要有人消费:本地预测先走、服务端复算兜底,UI 与玩法分开抢输入。
上游:游戏 UI 体系(玩家读状态的另一半)、前后端边界与网络层(意图可复算的权威口径)、客户端内部分层(UI 与玩法的消费边界);下游:预测、补偿与纠正(意图被服务端复算的地方)、帧同步详解(指令时序的确定性)。姊妹节点:脚本语言与 ECS(输入映射表的数据驱动与脚本层共享同一口径)。
Reference
- 本页为工程经验归纳;各平台输入 API(Gamepad、Pointer Events、触摸)以官方文档为准;页内实战案例为教学示例(数字取整简化)
- 术语对照:Input Abstraction(输入抽象)、Input Mapping(输入映射)、Intent(意图)、Input Latency(输入延迟)