在现代 Web3D 开发中,高效的输入管理是创建流畅交互体验的关键。本文将详细介绍如何在 Babylon.js 中实现一个强大的鼠标输入管理单例,帮助你优雅地处理所有指针事件。
为什么需要鼠标输入管理单例?
在复杂的 3D 场景中,鼠标/指针输入管理面临几个挑战:
-
事件分散:多个对象需要响应鼠标事件
-
状态管理:需要跟踪鼠标位置、点击状态等
-
性能优化:避免重复注册事件监听器
-
代码组织:集中管理输入逻辑,避免分散在各处
单例模式完美解决了这些问题,它提供了:
-
全局唯一的访问点
-
统一的输入事件分发
-
更好的性能表现
-
更清晰的代码结构
基础实现
首先让我们看一个基础的 MouseController
实现:
import { Scene, PointerEventTypes, PointerInfo } from "@babylonjs/core";type PointerEventCallback = (pointerInfo: PointerInfo) => void;export class MouseController {private static instance: MouseController | null = null;private scene: Scene;private eventCallbacks: Map<PointerEventTypes, PointerEventCallback[]> = new Map();private constructor(scene: Scene) {this.scene = scene;this.initialize();}public static initialize(scene: Scene): MouseController {if (!MouseController.instance) {MouseController.instance = new MouseController(scene);}return MouseController.instance;}public static getInstance(): MouseController {if (!MouseController.instance) {throw new Error("MouseController not initialized. Call initialize() first.");}return MouseController.instance;}private initialize(): void {Object.values(PointerEventTypes).forEach(eventType => {this.eventCallbacks.set(eventType, []);});this.scene.onPointerObservable.add((pointerInfo) => {const callbacks = this.eventCallbacks.get(pointerInfo.type);callbacks?.forEach(callback => callback(pointerInfo));});}public on(eventType: PointerEventTypes, callback: PointerEventCallback): void {this.eventCallbacks.get(eventType)?.push(callback);}public off(eventType: PointerEventTypes, callback: PointerEventCallback): void {const callbacks = this.eventCallbacks.get(eventType);if (callbacks) {const index = callbacks.indexOf(callback);if (index !== -1) callbacks.splice(index, 1);}}public static dispose(): void {if (MouseController.instance) {MouseController.instance.eventCallbacks.clear();MouseController.instance = null;}}
}
高级功能扩展
基础版本已经可用,但我们还可以添加更多实用功能:
1. 自定义高级事件
// 在 initialize 方法中添加特殊事件处理
this.scene.onPointerObservable.add((pointerInfo) => {// ...基础事件处理...switch(pointerInfo.type) {case PointerEventTypes.POINTERPICK:if (pointerInfo.pickInfo?.hit) {this.emit('meshClicked', {pointerInfo,mesh: pointerInfo.pickInfo.pickedMesh});}break;case PointerEventTypes.POINTERMOVE:if (pointerInfo.pickInfo?.hit) {this.emit('meshHover', {pointerInfo,mesh: pointerInfo.pickInfo.pickedMesh});}break;}
});
2. 鼠标状态跟踪
private mouseState = {position: { x: 0, y: 0 },isDown: false,lastClickedMesh: null as AbstractMesh | null
};// 在事件处理中更新状态
case PointerEventTypes.POINTERDOWN:this.mouseState.isDown = true;break;
case PointerEventTypes.POINTERUP:this.mouseState.isDown = false;break;
case PointerEventTypes.POINTERMOVE:this.mouseState.position.x = this.scene.pointerX;this.mouseState.position.y = this.scene.pointerY;break;
3. 双击检测
private lastClickTime = 0;// 在 POINTERPICK 处理中
const now = Date.now();
if (now - this.lastClickTime < 300) { // 300ms 内再次点击视为双击this.emit('doubleClick', { pointerInfo, mesh: pointerInfo.pickInfo.pickedMesh });
}
this.lastClickTime = now;
使用示例
// 初始化
const mouseController = MouseController.initialize(scene);// 监听事件
mouseController.on(PointerEventTypes.POINTERPICK, ({ pickInfo }) => {console.log('点击了:', pickInfo.pickedMesh?.name);
});mouseController.on('meshHover', ({ mesh }) => {console.log('鼠标悬停在:', mesh.name);
});// 获取鼠标状态
const currentPos = mouseController.getMousePosition();
console.log('当前鼠标位置:', currentPos);
性能优化技巧
-
事件节流:对高频事件如 POINTERMOVE 进行节流处理
import { throttle } from 'lodash-es';this.scene.onPointerObservable.add(throttle((pointerInfo) => {// 处理逻辑 }, 100));
-
按需监听:只在需要时添加监听器,及时移除
-
事件冒泡控制:对于复杂场景,使用
pointerInfo.skipOnPointerObservable
控制事件传播
与 Babylon.js 生态集成
MouseController
可以很好地与其他 Babylon.js 功能配合:
-
与 ActionManager 结合:优先使用单例处理全局事件,特定对象交互使用 ActionManager
-
与 GUI 系统集成:检测鼠标是否在 GUI 元素上
-
与物理引擎配合:基于鼠标点击实现物理交互
测试建议
为确保 MouseController
的可靠性,建议编写以下测试:
-
单例模式测试 - 确保全局唯一实例
-
事件分发测试 - 验证所有事件类型正确触发
-
内存泄漏测试 - 检查 dispose 方法是否彻底清理
-
性能测试 - 监控高频事件处理的性能影响
总结
本文介绍的 MouseController
单例模式为 Babylon.js 应用提供了:
-
集中化的输入管理 - 统一处理所有鼠标/指针事件
-
灵活的事件系统 - 支持原生和自定义事件
-
状态跟踪能力 - 实时掌握鼠标状态
-
性能优化基础 - 为复杂交互提供高效处理机制
-
良好的扩展性 - 方便添加新功能如手势识别等
这种模式特别适合中大型 Babylon.js 项目,能够显著提高代码的可维护性和交互体验的一致性。你可以根据项目需求进一步扩展这个基础实现,比如添加触摸支持或游戏手柄输入集成。