自定义Gamepad类绑定GamepadEvent报错,如何实现可注册的自定义手柄类?
如何创建可注册到GamepadEvent的自定义Gamepad类实现?
我尝试构建一个作为Gamepad类的虚拟控制器并进行注册,复制了Gamepad类的结构创建了MyJoystick类:
class MyJoystick { readonly axes: ReadonlyArray<number>; readonly buttons: ReadonlyArray<GamepadButton>; readonly connected: boolean; readonly hapticActuators: ReadonlyArray<GamepadHapticActuator>; readonly id: string; readonly index: number; readonly mapping: GamepadMappingType; readonly timestamp: DOMHighResTimeStamp; constructor() { this.axes = []; this.buttons = []; this.connected = true; this.hapticActuators = []; this.id = "my-id"; this.index = 200; this.mapping = "standard"; this.timestamp = Math.floor(Date.now() / 1000); } }
使用以下代码分发事件:
const event = new GamepadEvent("gamepadconnected", { gamepad: new MyJoystick() as Gamepad }) window.dispatchEvent(event);
但出现报错:
Failed to construct 'GamepadEvent': Failed to read the 'gamepad' property from 'GamepadEventInit': Failed to convert value to 'Gamepad'. TypeError: Failed to construct 'GamepadEvent': Failed to read the 'gamepad' property from 'GamepadEventInit': Failed to convert value to 'Gamepad'.
问题原因
浏览器对GamepadEvent的gamepad参数有严格的内部类型校验,必须是浏览器原生生成的Gamepad实例,自定义类即便结构完全一致,也无法通过校验。
解决方案
方法1:拦截navigator.getGamepads注入虚拟控制器
通过重写navigator.getGamepads方法,将自定义虚拟控制器加入返回结果,同时触发gamepadconnected事件。大多数应用会在事件触发后调用getGamepads获取控制器,因此无需将自定义对象传入事件参数。
class MyJoystick { readonly axes: ReadonlyArray<number>; readonly buttons: ReadonlyArray<GamepadButton>; readonly connected: boolean; readonly hapticActuators: ReadonlyArray<GamepadHapticActuator>; readonly id: string; readonly index: number; readonly mapping: GamepadMappingType; readonly timestamp: DOMHighResTimeStamp; constructor() { this.axes = [0, 0]; // 初始化摇杆位置 this.buttons = Array(16).fill({ pressed: false, touched: false, value: 0 }); // 模拟标准按键组 this.connected = true; this.hapticActuators = []; this.id = "my-virtual-joystick"; this.index = this.getAvailableIndex(); // 获取未被占用的索引 this.mapping = "standard"; this.timestamp = performance.now(); } // 查找可用的控制器索引 getAvailableIndex(): number { const nativePads = navigator.getGamepads() || []; for (let i = 0; i < 4; i++) { // 通常浏览器最多识别4个控制器 if (!nativePads[i]) return i; } return 4; // fallback索引 } // 更新摇杆轴值 updateAxes(x: number, y: number) { (this.axes as number[]) = [Math.max(-1, Math.min(1, x)), Math.max(-1, Math.min(1, y))]; this.timestamp = performance.now(); } // 更新按键状态 updateButton(index: number, pressed: boolean) { if (index >= 0 && index < this.buttons.length) { (this.buttons[index] as any) = { pressed, touched: pressed, value: pressed ? 1 : 0 }; this.timestamp = performance.now(); } } } // 保存原生方法 const originalGetGamepads = navigator.getGamepads.bind(navigator); const virtualJoystick = new MyJoystick(); // 重写getGamepads注入虚拟控制器 navigator.getGamepads = function() { const nativePads = originalGetGamepads() || []; nativePads[virtualJoystick.index] = virtualJoystick as unknown as Gamepad; return nativePads; }; // 触发连接事件 window.dispatchEvent(new GamepadEvent("gamepadconnected", { // 若有真实控制器可传入,否则传null不影响逻辑 gamepad: originalGetGamepads()?.[0] || null }));
方法2:Proxy包装原生Gamepad(依赖真实控制器)
如果页面已连接真实控制器,可以用Proxy包装原生实例,修改属性模拟虚拟控制器,这种方法可直接将包装后的对象传入GamepadEvent。
const realPad = navigator.getGamepads()[0]; if (realPad) { const virtualPad = new Proxy(realPad, { get(target, prop) { switch(prop) { case 'id': return 'my-virtual-joystick'; case 'axes': return [0.5, 0]; // 自定义摇杆位置 case 'buttons': return Array(16).fill({ pressed: false, touched: false, value: 0 }); default: return Reflect.get(target, prop); } } }); window.dispatchEvent(new GamepadEvent("gamepadconnected", { gamepad: virtualPad })); }
注意事项
- 方法1无需依赖真实控制器,适用性更广
- 虚拟控制器的
index需避免与真实控制器冲突,建议通过遍历原生结果获取可用索引 - 由于
axes和buttons是只读数组,更新时需用类型断言绕过TS校验(运行时可修改) - 重写
navigator.getGamepads需在页面加载早期执行,避免被其他代码覆盖
内容的提问来源于stack exchange,提问作者bemeyer
相关产品推荐
相关产品推荐

