You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 17:34:57