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

Three.js中XR控制器自定义双击事件派发问题求助

在Three.js XR控制器中实现双击自定义事件的问题解决

问题背景

我是Typescript新手,正在Three.js应用里给手部输入控制器做双击事件,思路很直接:

  • 监听控制器的squeezestart事件
  • 记录上次点击的时间,计算两次点击的间隔
  • 间隔小于设定阈值时,派发自定义的sandbox:doubleClick事件

但实际跑起来发现:用document.dispatchEvent()能正常触发这个自定义事件,但从XRTargetRaySpace/XRGripSpace/XRHandSpace这类控制器对象派发时完全没反应,搞不懂这类对象的事件派发机制到底是怎么回事。

原实现代码:

let deltaTime = 0,
    lastclick = 0,
    MAX_DELTA_TIME = 500;
const doubleClickEvent = new CustomEvent("sandbox:doubleClick", {
    bubbles: true,
    cancelable: true,
    composed: true,
    detail: {
        deltaTime,
    },
});

controller1.addEventListener("squeezestart", (_) => {
    deltaTime = window.performance.now() - lastclick;

    if (deltaTime < MAX_DELTA_TIME) {
        controller1.dispatchEvent(doubleClickEvent);
        lastclick = 0;
    }
    lastclick = window.performance.now();
});
controller2.addEventListener("squeezestart", (_) => {
    deltaTime = window.performance.now() - lastclick;

    if (deltaTime < MAX_DELTA_TIME) {
        controller2.dispatchEvent(doubleClickEvent);
        lastclick = 0;
    }
    lastclick = window.performance.now();
});

期望的效果是能通过下面的代码监听到事件:

controller1.addEventListener('sandbox:doubleClick', (e) => console.log(e)) 

问题根源

  1. Three.js事件系统和DOM事件不通用:XR控制器是Three.js的Object3D子类,用的是Three.js自己的EventDispatcher事件机制,不是浏览器的DOM事件系统。你用DOM的CustomEvent去派发,控制器根本识别不了。
  2. 事件实例不能重复使用:DOM的CustomEvent一旦被派发过,就会被标记为已完成,没法再派发给其他对象或者重复触发,你代码里复用同一个事件实例,就算兼容也只能触发一次。
  3. 全局变量导致逻辑混乱:两个控制器共用同一个lastclick变量,比如控制器1点一下,控制器2紧接着点一下,会被误判成双击,逻辑完全错误。

修正后的实现代码

import { Event } from 'three';

// 给每个控制器单独存储点击状态,避免互相干扰
interface ControllerClickState {
  lastClickTime: number;
}

const MAX_DELTA_TIME = 500;
// 用Map关联控制器和它的专属点击状态
const controllerStates = new Map<XRTargetRaySpace | XRGripSpace | XRHandSpace, ControllerClickState>([
  [controller1, { lastClickTime: 0 }],
  [controller2, { lastClickTime: 0 }]
]);

// 每次触发双击时创建新的Three.js事件实例
function createDoubleClickEvent(deltaTime: number) {
  return new Event('sandbox:doubleClick', {
    bubbles: true,
    cancelable: true,
    detail: { deltaTime } // 将间隔时间放到detail中传递给监听者
  });
}

// 通用的双击处理逻辑,不用重复写两遍
function handleControllerSqueezeStart(event: Event) {
  const controller = event.target as XRTargetRaySpace | XRGripSpace | XRHandSpace;
  const state = controllerStates.get(controller);
  if (!state) return;

  const now = window.performance.now();
  const deltaTime = now - state.lastClickTime;

  if (deltaTime < MAX_DELTA_TIME) {
    // 创建新事件并派发
    const doubleClickEvent = createDoubleClickEvent(deltaTime);
    controller.dispatchEvent(doubleClickEvent);
    // 重置点击时间,防止连续触发多次双击
    state.lastClickTime = 0;
  } else {
    state.lastClickTime = now;
  }
}

// 给两个控制器绑定监听
controller1.addEventListener('squeezestart', handleControllerSqueezeStart);
controller2.addEventListener('squeezestart', handleControllerSqueezeStart);

关键修正说明

  1. 换用Three.js原生Event:Three.js的事件系统只识别自身的Event类实例,所以替换DOM的CustomEvent为Three.js的Event,还能通过detail属性传递额外数据。
  2. 每个控制器单独维护状态:用Map给每个控制器存储专属的上次点击时间,彻底解决跨控制器的误判问题。
  3. 每次派发新事件实例:每次触发双击都新建一个事件对象,避免因旧实例被标记为已完成而无法触发。
  4. 统一逻辑减少重复:把双击判断逻辑抽成通用函数,不用给多个控制器写重复代码,后续扩展更方便。

测试监听

现在可以正常监听控制器的双击事件了:

controller1.addEventListener('sandbox:doubleClick', (e) => {
  console.log('控制器1双击触发,间隔时间:', e.detail?.deltaTime);
});

controller2.addEventListener('sandbox:doubleClick', (e) => {
  console.log('控制器2双击触发,间隔时间:', e.detail?.deltaTime);
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 15:35:04