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))
问题根源
- Three.js事件系统和DOM事件不通用:XR控制器是Three.js的
Object3D子类,用的是Three.js自己的EventDispatcher事件机制,不是浏览器的DOM事件系统。你用DOM的CustomEvent去派发,控制器根本识别不了。 - 事件实例不能重复使用:DOM的
CustomEvent一旦被派发过,就会被标记为已完成,没法再派发给其他对象或者重复触发,你代码里复用同一个事件实例,就算兼容也只能触发一次。 - 全局变量导致逻辑混乱:两个控制器共用同一个
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);
关键修正说明
- 换用Three.js原生Event:Three.js的事件系统只识别自身的
Event类实例,所以替换DOM的CustomEvent为Three.js的Event,还能通过detail属性传递额外数据。 - 每个控制器单独维护状态:用
Map给每个控制器存储专属的上次点击时间,彻底解决跨控制器的误判问题。 - 每次派发新事件实例:每次触发双击都新建一个事件对象,避免因旧实例被标记为已完成而无法触发。
- 统一逻辑减少重复:把双击判断逻辑抽成通用函数,不用给多个控制器写重复代码,后续扩展更方便。
测试监听
现在可以正常监听控制器的双击事件了:
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
相关产品推荐
相关产品推荐

