VueUse useGamepad 实战:在 Vue 3 应用中响应式接入 Gamepad API(Airi 仓库参考指南)

发布时间:2026/9/11 7:33:01
VueUse useGamepad 实战:在 Vue 3 应用中响应式接入 Gamepad API(Airi 仓库参考指南) VueUse useGamepad 实战在 Vue 3 应用中响应式接入 Gamepad APIAiri 仓库参考指南【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi本文基于 Airi 开源仓库中的 useGamepad 参考文档 展开结合仓库内 VueUse 依赖与技能库配置系统讲解如何用useGamepad组合式函数把浏览器 Gamepad API 无缝接入 Vue 3 应用从读取手柄状态、轮询控制、连接事件、振动反馈到按键映射与 SSR 兼容读完即可直接落地到真实项目如桌面虚拟伙伴、游戏助手或 Web 控制台前端中。为什么需要useGamepadGamepad API 的响应式封装浏览器原生提供了 Gamepad APIMDN 标准接口用于读取连接的游戏手柄Gamepad状态但原生 API 有两个明显短板无响应式原生 API 采用命令式查询需要手动在每一帧读取navigator.getGamepads()无法与 Vue 的响应式状态系统直接协作无事件机制手柄状态变化如按钮按下、摇杆移动不会派发 DOM 事件必须靠轮询才能感知变化。useGamepad正是针对这两点设计的 VueUse 组合式函数Composable它位于 VueUse 的 Browser 分类下为该分类中唯一处理游戏手柄输入的函数。在本仓库的 vueuse-functions 技能表 中其描述为 Provides reactive bindings for the Gamepad APIInvocation 规则为AUTO——即只要场景适用Vue 3 / Nuxt 3 及以上项目中出现手柄交互需求就应当优先考虑使用它而不是手写原生轮询逻辑。在 Airi 仓库的技术栈中这一用法完全成立仓库内所有前端应用如 stage-tamagotchi 应用、stage-web、stage-pocket、component-calling 等均通过 pnpm catalog 统一依赖vueuse/core因此useGamepad及相关工具函数开箱即用无需额外安装依赖。快速上手在组件中读取手柄状态使用前提先与页面交互由于 Gamepad API 的工作方式手柄在被检测到之前用户必须先用游戏手柄与页面进行过一次交互例如按一下手柄上的任意按键。这是浏览器出于安全与隐私考虑的行为并非useGamepad的限制。排查为什么检测不到手柄时请优先检查这一步。最小可用示例script setup langts import { useGamepad } from vueuse/core import { computed } from vue const { isSupported, gamepads } useGamepad() const gamepad computed(() gamepads.value.find(g g.mapping standard)) /script template span {{ gamepad.id }} /span /template代码要点说明isSupported表示当前环境浏览器 页面上下文是否支持 Gamepad API可用于在模板或逻辑中做降级处理gamepads一个RefGamepad[]保存当前已连接的全部手柄对象Gamepad为标准 DOM 类型随轮询自动更新mapping standardGamepad.mapping为标准布局手柄如 Xbox 兼容手柄时值为standard通过Array.prototype.find可筛选出布局最标准的那只手柄避免拿到未知布局的设备gamepad.id手柄的标识字符串通常包含厂商与型号信息不同浏览器格式略有差异适合直接展示给用户确认连接的是哪只手柄。状态轮询机制requestAnimationFrame与pause/resume这是useGamepad最核心的底层设计。Gamepad API目前没有事件支持来更新手柄状态因此useGamepad内部通过requestAnimationFrame循环轮询手柄状态让gamepads这个 ref 保持最新。你可以通过返回的pause和resume函数手动控制这一轮询import { useGamepad } from vueuse/core const { pause, resume, gamepads } useGamepad() pause() // 调用 pause() 之后gamepads 对象将不再更新 resume() // 调用 resume() 之后gamepads 对象将在用户输入时恢复更新实战价值性能优化当游戏手柄 UI 面板不可见例如弹层关闭时调用pause()暂停轮询可避免无谓的逐帧查询开销面板重新打开时再resume()快照语义在需要冻结当前手柄状态用于比较的场景如记录按下前的基线值pause()能让gamepads保持为历史快照与 rAF 生态的关系这一用 rAF 驱动响应式状态的模式在 VueUse 中很常见仓库参考库中同样收录了底层工具 useRafFn每帧调用函数以及相关节流/防抖系列函数可组合出更精细的输入采样策略。连接与断开事件onConnected/onDisconnected手柄的热插拔连接/断开有对应事件useGamepad通过onConnected和onDisconnected暴露出来分别在手柄连接或断开时触发。回调参数为手柄在gamepads数组中的索引import { useGamepad } from vueuse/core // ---cut--- const { gamepads, onConnected, onDisconnected } useGamepad() onConnected((index) { console.log(${gamepads.value[index].id} connected) }) onDisconnected((index) { console.log(${index} disconnected) })注意事项两个回调的类型均为EventHookOnnumber——这是 VueUse 基于createEventHook的事件钩子约定仓库参考库中也收录了 createEventHook 的完整说明回调在组件卸载时会自动清理无需手动移除监听连接回调中gamepads.value[index]在触发时通常已包含新手柄数据可以直接读取其id做手柄已接入的提示或默认选中断开回调中索引对应的槽位可能已被移除或置空因此示例只打印索引如需读取设备信息应在连接时缓存。典型应用连接提示 Toast、断线自动暂停游戏、按连接顺序为多手柄分配 P1/P2 角色。手柄振动Gamepad Haptics API 的响应式接入Gamepad Haptics API 目前覆盖较稀疏不同浏览器对手柄振动GamepadHapticActuator的支持差异明显使用前建议先查阅 MDN 的浏览器兼容性表确认目标浏览器是否支持。useGamepad直接暴露原生Gamepad对象因此手柄振动需要你自己基于hapticActuators封装import { useGamepad } from vueuse/core // ---cut--- import { computed } from vue const { gamepads, onConnected, onDisconnected } useGamepad() const gamepad gamepads.value[0]! const supportsVibration computed(() gamepad.hapticActuators.length 0) function vibrate() { if (supportsVibration.value) { const actuator gamepad.hapticActuators[0] actuator.playEffect(dual-rumble, { startDelay: 0, duration: 1000, weakMagnitude: 1, strongMagnitude: 1, }) } }参数语义dual-rumble双马达效果参数含义取值范围startDelay开始振动前的延迟毫秒数0表示立即开始duration振动持续时长毫秒数示例为10001 秒weakMagnitude弱马达高频强度0~1示例为1满强度strongMagnitude强马达低频强度0~1示例为1满强度两个马达分别负责不同频段强马达产生厚重的低频震动如碰撞、爆炸弱马达产生细腻的高频震动如 UI 反馈。通过调节weakMagnitude与strongMagnitude的比例可以做出差异化手感。由于gamepad.hapticActuators可能为空数组先通过computed计算supportsVibration再调用playEffect可以避免在不支持振动的设备上抛错。按键映射mapGamepadToXbox360Controller原生 Gamepad API 的按钮和摇杆都按索引访问gamepad.buttons[0]、gamepad.axes[1]可读性差且容易记错。useGamepad配套提供了映射函数mapGamepadToXbox360Controller把标准手柄standard mapping映射为 Xbox 360 手柄的具名按键布局script setup import { mapGamepadToXbox360Controller } from vueuse/core const controller mapGamepadToXbox360Controller(gamepad) /script template span{{ controller.buttons.a.pressed }}/span span{{ controller.buttons.b.pressed }}/span span{{ controller.buttons.x.pressed }}/span span{{ controller.buttons.y.pressed }}/span /template使用后即可用controller.buttons.a.pressed这类语义化写法代替gamepad.buttons[0].pressed代码自解释性大幅提升。从仓库文档中的类型声明看映射结果的结构完整覆盖了 Xbox 360 手柄的全部输入面buttonsa/b/x/y四个主按键均为GamepadButton含pressed/touched/value字段bumper左右肩键left/righttriggers左右扳机left/right作为按钮暴露stick左右摇杆每个摇杆含horizontal/vertical两个轴值number与button按下摇杆dpad方向键up/down/left/rightback与start功能键。目前官方仅提供了 Xbox 360 手柄的映射。如果你有其他手柄如 PS 系布局需要映射可以向 VueUse 提交 PR 扩充映射集合。注意函数签名要求传入RefGamepad | undefined返回值为ComputedRef映射对象 | null——当手柄不可用时会得到null模板中直接渲染前应做好空值防护例如用v-if或可选链。SSR 兼容性与 hydration 处理useGamepad面向浏览器端client-side设计。在服务端渲染SSR场景下服务端没有navigator/gamepad环境某些情况下可能引起 hydration 不匹配服务端渲染的标记与客户端首次渲染不一致。处理方案按框架区分Nuxt 项目把组件文件重命名为.client.vue后缀例如GamepadComponent.client.vueNuxt 会自动让该组件仅在客户端渲染从根源上规避 hydration 不匹配其他框架或纯 Vue 项目将使用手柄的组件包裹在ClientOnly组件中确保其只在客户端挂载。结合类型声明可以看到useGamepad的选项类型UseGamepadOptions继承自ConfigurableWindow与ConfigurableNavigatorVueUse 的可配置 window / navigator约定即你可以传入自定义的window与navigator引用例如在测试环境或 Electron 定制环境中替换这同样是其 SSR/测试友好性的体现。类型声明全景选项、返回值与映射签名仓库参考文档给出的完整类型声明如下是精确理解 API 契约的第一手资料export interface UseGamepadOptions extends ConfigurableWindow, ConfigurableNavigator {} export interface UseGamepadReturn extends Supportable, Pausable { onConnected: EventHookOnnumber onDisconnected: EventHookOnnumber gamepads: RefGamepad[] } /** * Maps a standard standard gamepad to an Xbox 360 Controller. */ export declare function mapGamepadToXbox360Controller( gamepad: RefGamepad | undefined, ): ComputedRef{ buttons: { a: GamepadButton b: GamepadButton x: GamepadButton y: GamepadButton } bumper: { left: GamepadButton right: GamepadButton } triggers: { left: GamepadButton right: GamepadButton } stick: { left: { horizontal: number vertical: number button: GamepadButton } right: { horizontal: number vertical: number button: GamepadButton } } dpad: { up: GamepadButton down: GamepadButton left: GamepadButton right: GamepadButton } back: GamepadButton start: GamepadButton } | null export declare function useGamepad( options?: UseGamepadOptions, ): UseGamepadReturn要点解读UseGamepadReturn继承Supportable提供isSupported与Pausable提供pause/resume/isActive等 Pausable 约定成员事件钩子与gamepadsref 一同返回所有类型均为导出公开类型可直接import type { UseGamepadOptions, UseGamepadReturn } from vueuse/core用于业务代码的类型标注。在 Airi 仓库中的定位与实践建议useGamepad参考文档位于仓库的 .agents/skills/vueuse-functions/references/useGamepad.md它是仓库内 Agent 技能体系vueuse-functions 技能的一部分该技能要求开发助手在 Vue.js / Nuxt 项目开发中优先检查 VueUse 函数是否可满足需求优先组合式函数而非自造轮子。useGamepad在技能表中归类于Browser分类且为AUTO调用级别——只要场景中需要手柄输入就应默认选用。对于 Airi 这类桌面虚拟伙伴 / 数字生命项目Web / macOS / Windows 多端支持手柄输入的潜在场景很典型舞台交互控制通过手柄方向键与 A/B/X/Y 按键驱动虚拟形象互动或触发表情游戏联动项目描述中明确涉及 Minecraft 等游戏玩法手柄可作为游戏操作之外的附加输入通道Electron 桌面端Electron 渲染进程同样运行 ChromiumGamepad API 可用useGamepad可以无差别工作在 Web 与桌面端渲染器中前提是系统能识别手柄设备。需要特别说明的是从当前仓库源码检索结果看useGamepad尚未被仓库内的业务代码直接调用它更多是作为 Agent 技能参考库中的标准答案存在——当未来需求触达手柄输入时开发与 Agent 都应优先采用本指南描述的方式接入而不是手写原生轮询。接入时的依赖前提vueuse/core已由仓库根 pnpm-workspace.yaml 的 catalog 统一管理所有应用与包均可用无需额外配置。小结useGamepad用约几十行源码解决了原生 Gamepad API 的两个核心痛点把命令式查询封装为响应式 ref并把逐帧轮询内建为可暂停的生命周期任务。配合onConnected/onDisconnected事件钩子、mapGamepadToXbox360Controller语义化映射与 Haptics 振动封装可以在一两个文件内构建出完整的手柄输入层。遵循仓库技能库的AUTO指引在 Vue 3 / Nuxt 项目中遇到手柄需求时优先引入useGamepad即可保持实现简洁、可维护且与 VueUse 生态一致。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考