实现 Babylon.js 鼠标输入管理单例 (MouseController) 的最佳实践

发布于:2025-05-01 ⋅ 阅读:(30) ⋅ 点赞:(0)

在现代 Web3D 开发中,高效的输入管理是创建流畅交互体验的关键。本文将详细介绍如何在 Babylon.js 中实现一个强大的鼠标输入管理单例,帮助你优雅地处理所有指针事件。

为什么需要鼠标输入管理单例?

在复杂的 3D 场景中,鼠标/指针输入管理面临几个挑战:

  1. 事件分散:多个对象需要响应鼠标事件

  2. 状态管理:需要跟踪鼠标位置、点击状态等

  3. 性能优化:避免重复注册事件监听器

  4. 代码组织:集中管理输入逻辑,避免分散在各处

单例模式完美解决了这些问题,它提供了:

  • 全局唯一的访问点

  • 统一的输入事件分发

  • 更好的性能表现

  • 更清晰的代码结构

基础实现

首先让我们看一个基础的 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);

性能优化技巧

  1. 事件节流:对高频事件如 POINTERMOVE 进行节流处理

    import { throttle } from 'lodash-es';
    
    this.scene.onPointerObservable.add(throttle((pointerInfo) => {
        // 处理逻辑
    }, 100));

  2. 按需监听:只在需要时添加监听器,及时移除

  3. 事件冒泡控制:对于复杂场景,使用 pointerInfo.skipOnPointerObservable 控制事件传播

与 Babylon.js 生态集成

MouseController 可以很好地与其他 Babylon.js 功能配合:

  1. 与 ActionManager 结合:优先使用单例处理全局事件,特定对象交互使用 ActionManager

  2. 与 GUI 系统集成:检测鼠标是否在 GUI 元素上

  3. 与物理引擎配合:基于鼠标点击实现物理交互

测试建议

为确保 MouseController 的可靠性,建议编写以下测试:

  1. 单例模式测试 - 确保全局唯一实例

  2. 事件分发测试 - 验证所有事件类型正确触发

  3. 内存泄漏测试 - 检查 dispose 方法是否彻底清理

  4. 性能测试 - 监控高频事件处理的性能影响

总结

本文介绍的 MouseController 单例模式为 Babylon.js 应用提供了:

  1. 集中化的输入管理 - 统一处理所有鼠标/指针事件

  2. 灵活的事件系统 - 支持原生和自定义事件

  3. 状态跟踪能力 - 实时掌握鼠标状态

  4. 性能优化基础 - 为复杂交互提供高效处理机制

  5. 良好的扩展性 - 方便添加新功能如手势识别等

这种模式特别适合中大型 Babylon.js 项目,能够显著提高代码的可维护性和交互体验的一致性。你可以根据项目需求进一步扩展这个基础实现,比如添加触摸支持或游戏手柄输入集成。


网站公告

今日签到

点亮在社区的每一天
去签到