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

移动端双Joystick独立触摸事件异常修复求助

双摇杆触摸事件冲突的修复方案

问题根源

你的代码中所有触摸移动事件都固定取e.touches[0],当同时触摸两个摇杆时,两个Joystick实例都会响应同一个触摸点,导致第二个摇杆复制第一个的轨迹。移动端触摸事件支持多点触摸,每个触摸点有唯一的identifier,必须通过这个ID来绑定摇杆和对应的触摸操作。

修复后的完整代码

class Joystick {
    constructor(joystickElement, stickElement, stickNumber) {
        this.joystickElement = joystickElement;
        this.stickElement = stickElement;
        this.isActive = false;
        this.touchIdentifier = null; // 新增:存储当前触摸点的唯一ID
        this.initialPositionX = 0;
        this.initialPositionY = 0;
        this.cursorOffsetX = 0;
        this.cursorOffsetY = 0;
        this.stickNumber = stickNumber;

        stickElement.addEventListener('touchstart', (e) => this.onTouchStart(e));
        document.addEventListener('touchmove', (e) => this.onTouchMove(e));
        document.addEventListener('touchend', (e) => this.onTouchEnd(e));
        document.addEventListener('touchcancel', (e) => this.onTouchEnd(e)); // 补充处理触摸取消事件
    }

    onTouchStart(e) {
        e.preventDefault();
        this.isActive = true;
        // 记录当前触摸点的唯一ID
        this.touchIdentifier = e.changedTouches[0].identifier;
        this.stickElement.style.transition = '0s';
        const joystickRect = this.joystickElement.getBoundingClientRect();
        const touch = e.changedTouches[0];
        this.initialPositionX = touch.clientX - joystickRect.left - this.stickElement.offsetWidth / 2;
        this.initialPositionY = touch.clientY - joystickRect.top - this.stickElement.offsetHeight / 2;
        this.cursorOffsetX = this.stickElement.offsetWidth / 2;
        this.cursorOffsetY = this.stickElement.offsetHeight / 2;
    }

    onTouchMove(e) {
        if (this.isActive && this.touchIdentifier !== null) {
            // 找到当前摇杆对应的触摸点
            const touch = Array.from(e.touches).find(t => t.identifier === this.touchIdentifier);
            if (!touch) return; // 找不到对应触摸点则退出
            const joystickRect = this.joystickElement.getBoundingClientRect();
            this.moveStick(
                touch,
                joystickRect,
                this.stickElement,
                this.cursorOffsetX,
                this.cursorOffsetY,
                this.initialPositionX,
                this.initialPositionY,
                this.stickNumber
            );
        }
    }

    onTouchEnd(e) {
        // 只有当离开的触摸点是当前摇杆绑定的触摸点时,才重置状态
        const endedTouch = Array.from(e.changedTouches).find(t => t.identifier === this.touchIdentifier);
        if (endedTouch) {
            this.isActive = false;
            this.touchIdentifier = null;
            this.stickElement.style.transition = '0.2s';
            this.resetStickPosition(this.stickElement, this.stickNumber);
        }
    }

    moveStick(touch, joystick, stick, cursorOffsetX, cursorOffsetY, initialPositionX, initialPositionY, stickNumber) {
        const x = touch.clientX - joystick.left - cursorOffsetX - initialPositionX;
        const y = touch.clientY - joystick.top - cursorOffsetY - initialPositionY;

        const stickRadius = stick.offsetWidth / 2;
        const maxDistance = joystick.width / 2 - stickRadius + 30;

        const distance = Math.sqrt(x * x + y * y);
        if (distance <= maxDistance) {
            stick.style.transform = `translate(${x - 32}px, ${y - 32}px)`;
        } else {
            const angle = Math.atan2(y, x);
            const newX = Math.cos(angle) * maxDistance;
            const newY = Math.sin(angle) * maxDistance;
            stick.style.transform = `translate(${newX - 32}px, ${newY - 32}px)`;
        }

        switch (stickNumber) {
            case '1':
                document.getElementById('stick1x').textContent = x;
                document.getElementById('stick1y').textContent = y;
                break;
            case '2':
                document.getElementById('stick2x').textContent = x;
                document.getElementById('stick2y').textContent = y;
                break;
        }
    }

    resetStickPosition(stick, stickNumber) {
        stick.style.transform = 'translate(-50%, -50%)';

        switch (stickNumber) {
            case '1':
                document.getElementById('stick1x').textContent = '0';
                document.getElementById('stick1y').textContent = '0';
                break;
            case '2':
                document.getElementById('stick2x').textContent = '0';
                document.getElementById('stick2y').textContent = '0';
                break;
        }
    }
}

const joystick1 = new Joystick(document.getElementById('joystick1'), document.getElementById('stick1'), '1');
const joystick2 = new Joystick(document.getElementById('joystick2'), document.getElementById('stick2'), '2');

关键修改点

  • 新增touchIdentifier属性,绑定当前摇杆对应的触摸点唯一ID
  • touchstart时记录触摸点的identifier,而不是直接取第一个触摸点
  • touchmove时遍历所有触摸点,找到与当前摇杆绑定的ID匹配的触摸点再处理
  • touchend和touchcancel事件中,只有当离开的触摸点是当前摇杆绑定的ID时,才重置状态
  • 替换innerHTML为textContent,避免不必要的HTML解析,提升性能

内容的提问来源于stack exchange,提问作者Anton Virovets

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 15:06:01