Phaser 键盘与手柄改键系统:动作映射、冲突检测和本地保存要统一

讲解 Phaser 游戏中键盘、手柄和移动端虚拟按键的动作映射方案,覆盖改键 UI、冲突检测、存档、默认方案和无障碍。

改键系统不是设置页里的附加功能

很多 Phaser 游戏最初只写键盘输入:WASD 移动,空格跳跃,鼠标攻击。等玩家开始反馈“我想用方向键”“手柄 A/B 反了”“笔记本键位冲突”“左撇子不舒服”时,团队才补一个改键界面。若一开始输入直接散落在各个 Scene 中,补改键会非常痛苦:战斗 Scene 读空格,菜单 Scene 读 Enter,教程写死“按 W”,移动端按钮又是另一套逻辑。输入系统应该从一开始就以“动作”为核心,而不是以“按键”为核心。

动作映射的意思是:游戏逻辑只关心 jumpattackdashopenInventory,不关心它来自键盘、手柄、触摸还是辅助设备。InputMapper 把具体输入转换成动作状态。这样改键 UI 修改的是映射表,战斗系统仍然只读动作。这个边界能同时解决键盘、手柄、虚拟摇杆和教程文案的问题。

定义动作层

动作可以分为连续轴和离散按钮。移动方向通常是轴,攻击、跳跃、互动是按钮。按钮还要区分 pressed、held、released。Phaser 的 Keyboard 和 Gamepad API 提供原始状态,但项目层需要把它们统一成 ActionState。比如键盘 A/D、方向键、手柄左摇杆都能贡献 moveX;空格、手柄 A、屏幕跳跃按钮都能触发 jump

动作还应有上下文。菜单中的确认和战斗中的攻击可能使用同一个物理按键,但语义不同。可以分 ActionMap:gameplay、menu、dialogue、photoMode。当前 Scene 或全局状态决定启用哪个 map。不要让菜单打开时角色仍然攻击,也不要让战斗中按确认误触设置项。

flowchart TD
  A["Keyboard / Gamepad / Touch 原始输入"] --> B["DeviceAdapter 归一化按键和轴"]
  B --> C["InputMapper 按当前 ActionMap 转成动作"]
  C --> D["ConflictResolver 检查改键冲突"]
  C --> E["ActionState:pressed / held / released / axis"]
  E --> F["GameplaySystem:移动、跳跃、攻击"]
  E --> G["UISystem:确认、返回、导航"]
  H["SettingsStore:本地保存映射"] --> C

改键 UI 要先监听,再确认

改键流程最好是:玩家点击某个动作,系统进入监听状态,下一次有效输入成为候选,检查冲突,展示确认或替换选项。不要玩家一按键就立刻保存,因为他可能按到 Esc 想取消,也可能按到系统保留键。监听状态要有超时,支持取消,并过滤不适合作为绑定的输入,比如浏览器快捷键、音量键、系统返回键。

手柄改键还要处理轴。玩家轻推摇杆可能产生噪声,监听时要设置阈值,比如轴绝对值超过 0.6 才算输入。扳机有些浏览器表现为按钮,有些表现为轴,DeviceAdapter 要统一。改键 UI 上显示的名称也要人类可读:Gamepad0Button1 对玩家没有意义,应显示 “B / Circle” 或按平台映射显示。

冲突检测不是简单禁止重复

有些动作不能共用按键,比如跳跃和攻击;有些动作可以共用,比如菜单确认和对话继续,因为它们在不同上下文;有些轴正负方向属于一组,比如左和右不能绑定同一个按键。冲突检测要基于 action map 和冲突组,而不是全局禁止重复。否则玩家无法设置自己习惯的方案。

冲突解决可以提供三种策略:拒绝绑定、交换绑定、清空旧绑定。对普通玩家,提示“此按键已用于冲刺,是否替换?”最清楚。对高级玩家,可以允许多绑定,比如跳跃同时绑定空格和手柄 A。多绑定对可访问性很有帮助。

一个动作映射模型

下面的代码展示了动作映射的核心结构。真实项目中还要支持轴、组合键和设备识别。

type ActionName = "jump" | "attack" | "dash" | "interact" | "pause";
type InputBinding =
  | { device: "keyboard"; code: string }
  | { device: "gamepad"; button: number }
  | { device: "gamepadAxis"; axis: number; direction: -1 | 1 };

interface ActionBinding {
  action: ActionName;
  context: "gameplay" | "menu";
  bindings: InputBinding[];
  conflictGroup: string;
}

export function hasConflict(next: ActionBinding, all: ActionBinding[]) {
  return all.filter((item) =>
    item.action !== next.action &&
    item.context === next.context &&
    item.conflictGroup === next.conflictGroup &&
    item.bindings.some((binding) =>
      next.bindings.some((candidate) => JSON.stringify(candidate) === JSON.stringify(binding)),
    ),
  );
}

这段代码的重点是冲突按上下文判断。pause 在 gameplay 和 menu 中可以复用同一键,而 jumpdash 在 gameplay 中通常不应冲突。生产代码不要用 JSON.stringify 比较绑定,可以写稳定的 binding key。

本地保存与默认方案

改键配置应保存到本地,登录账号时也可以同步到云端。保存时记录版本。新增动作后,旧配置需要迁移:保留玩家已有绑定,为新动作填默认值。某个绑定在新版本被废弃时,要安全移除。不要因为新增一个“拍照模式”动作就重置玩家所有键位。

默认方案要按设备区分。桌面键盘一套,手柄一套,移动端虚拟按钮一套。第一次检测到手柄时,可以提示切换手柄布局,但不要强制覆盖键盘设置。玩家拔掉手柄后,键盘仍应可用。输入系统可以同时接受多个设备,最近使用的设备决定 UI 提示。

教程文案要读动作映射

如果玩家把跳跃改成 J,教程还显示“按空格跳跃”,这是很糟糕的体验。教程、按钮提示、HUD 快捷键都应该从 InputMapper 查询当前绑定。文案可以写成“按 {jump} 跳跃”,渲染时替换为当前设备的显示名称。手柄图标、键盘按键帽、触摸按钮都可以由同一套动作提示系统生成。

这也影响本地化。中文可以显示“按 A 跳跃”,英文显示 “Press A to jump”。动作名称本地化,按键名不一定翻译。不要把完整句子写死在教程配置里再手工维护多套设备文案。

手柄连接和断开

浏览器手柄事件并不总是稳定。有些手柄需要按按钮后才出现在 navigator.getGamepads();断开事件可能延迟;同一按钮在不同设备上的布局也可能不同。游戏应在每帧或固定间隔扫描手柄状态,并在检测到新设备时显示轻量提示。断开时,如果玩家正在战斗,可以自动暂停并提示重新连接或切换键盘。

手柄死区也要配置。摇杆轻微漂移很常见,默认死区可以设为 0.15 到 0.25。玩家可以在设置里调整。没有死区,角色会自己走;死区太大,细微移动不灵。这个问题在平台跳跃、射击瞄准和菜单导航中都很明显。

可访问性和安全边界

改键是可访问性的一部分。允许多绑定、长按替代连点、切换而非按住、调整摇杆死区、降低双键组合需求,都会帮助更多玩家。不要把“默认键位合理”当作终点。尤其是 Web 游戏,玩家设备差异很大,键盘布局也不同。使用 KeyboardEvent.code 能代表物理键位,key 能代表字符,二者选择会影响不同键盘布局。动作游戏通常优先 physical code,文本输入场景再用 key。

同时要避免绑定浏览器危险快捷键,比如刷新、关闭标签、开发者工具组合。无法完全拦截系统快捷键,但可以在改键监听时提示不推荐。全屏模式和嵌入式 WebView 下,某些按键行为还会不同,测试时要覆盖目标平台。

上线前检查清单

确认游戏逻辑只读取动作,不直接读物理按键;确认动作分上下文;确认改键监听有取消、超时和过滤;确认冲突检测按上下文和冲突组工作;确认新增动作不会重置旧配置;确认教程文案从当前映射生成;确认手柄连接、断开、死区和轴输入可用;确认移动端虚拟按钮也走同一动作层;确认设置导入导出和恢复默认清晰;确认低频设备问题有日志。

输入系统越早抽象,后面越省事。Phaser 的输入 API 足够灵活,但项目不能把原始输入散落在玩法里。把物理输入映射成动作,再让所有系统只认动作,改键、手柄、教程和可访问性就会自然连在一起。

组合键和长按动作

有些游戏需要组合键,比如防御加攻击释放反击,或者按住肩键切换技能轮盘。组合键不能简单写在玩法里,否则改键后会乱。InputMapper 可以支持 chord:一个动作由主键和修饰键组成。冲突检测时,单键动作和组合键动作要特别处理。如果 RB + A 是技能,单独 A 仍然可以是跳跃,但按住 RB 时应该优先触发技能上下文。

长按、连按、双击也应在动作层处理。比如长按打开地图,短按打开任务;双击方向冲刺。这些手势的阈值要可配置,并考虑无障碍。玩家如果开启“避免连按”,双击冲刺可以改成单独按钮。动作层统一处理后,教程和 UI 提示也能正确显示。

菜单导航和焦点系统

手柄支持不只是玩法控制,还包括菜单导航。Phaser UI 如果是 Canvas 绘制,需要自己的焦点系统:当前选中按钮、上下左右邻居、确认、返回、禁用状态。改键系统里的 menu action 提供导航输入,UI 系统消费它。不要让每个菜单自己监听方向键,否则焦点规则会不一致。

焦点系统要处理弹窗叠加。打开确认框时,底层菜单焦点暂停;关闭后恢复之前焦点。手柄断开时,焦点可保留但不强制显示;鼠标移动时,可以切换到鼠标模式。最近输入设备决定提示图标:键盘显示按键帽,手柄显示按钮图标,触摸隐藏按键提示。这个细节会让游戏显得完整。

配置导入导出和客服排查

改键问题很难远程排查。设置页可以提供“恢复默认”和“导出输入配置”。导出内容包括映射版本、设备、绑定列表和冲突状态。玩家反馈“按不了跳跃”时,客服或开发能看到跳跃是否被清空、是否绑定到不可用设备、是否被上下文覆盖。调试面板也应显示当前动作状态,按下物理键时显示它被解析成哪个动作。

云同步改键时要谨慎。玩家在桌面设置的键盘绑定同步到手机没有意义;手柄绑定跨平台也可能按钮编号不同。可以按设备类型分别保存,账号同步只同步同类设备的配置。没有匹配设备时使用默认方案,而不是套用不合适的旧配置。

浏览器焦点和输入丢失

Web 游戏还要处理浏览器焦点。玩家按住方向键时切出页面,回来后 keyup 事件可能丢失,角色会持续移动。InputSystem 在失焦时应清空所有 held 状态,恢复后等待新的输入。手柄断开、蓝牙休眠、移动端系统手势打断也类似。动作状态必须有全局 reset。

设置页监听改键时,也要暂停玩法输入。否则玩家想绑定攻击键,角色在背景里攻击。改键弹窗打开后,GameplayActionMap 暂停,只保留 UI 和监听逻辑。关闭弹窗后恢复原上下文。输入系统和暂停系统要协作,不能各管各的。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页