Skip to content

鼠标事件与触摸事件

第一个前端工程中,我们看到了引擎初始化的过程,其中是会从document上获取canvas对象用于渲染,另外是获取一个element对象来响应鼠标事件的touchElement,如果没有额外的事件响应对象,那么这个对象就是用于渲染的canvas对象。

typescript
export const initEngine = (): Promise<boolean> => {
    const rlt = new Promise<boolean>((resolve) => {
        const urlSearch = new URLSearchParams(location.search);
        const backend: string = urlSearch.get('backend') || 'webgl';

        registerSImpl();

        RoyEngine.init(
            ['./assets/favicon.ico'],
            () => {
                const mainCanvas = document.getElementById('layout-canvas') as HTMLCanvasElement;
                const touchCanvas = document.getElementById('layout-touch') as HTMLCanvasElement;

                const elementData: ElementData = {
                    mainCanvas: mainCanvas,
                    touchCanvas: touchCanvas,
                    stretchCanvas: true,

                    hireachyPanel: document.getElementById('layout-hireachy'),
                    inspectorPanel: document.getElementById('layout-inspector'),
                    assetsPanel: document.getElementById('layout-assets'),
                };
                _GAppNativePC = new AppNativePC(elementData);
                window['app'] = _GAppNativePC;

                resolve(true);
            },
            backend,
        );
    });

    return rlt;
};

该对象传入AppClient之后,会绑定鼠标事件,而无需在应用侧再次绑定,应用侧只需要按照一定的规则来响应事件即可。

typescript

this.render = this.render.bind(this);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);

this.onMouseDown = this.onMouseDown.bind(this);
this.m_elementData.touchCanvas.addEventListener('mousedown', this.onMouseDown, false);
this.onMouseMove = this.onMouseMove.bind(this);
this.m_elementData.touchCanvas.addEventListener('mousemove', this.onMouseMove, false);
this.onMouseUp = this.onMouseUp.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseup', this.onMouseUp, false);

this.onMouseEnter = this.onMouseEnter.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseenter', this.onMouseEnter, false);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.m_elementData.touchCanvas.addEventListener('mouseleave', this.onMouseLeave, false);

this.onTouchStart = this.onTouchStart.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchstart', this.onTouchStart, true);
this.onTouchMove = this.onTouchMove.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchmove', this.onTouchMove, true);
this.onTouchCancel = this.onTouchCancel.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchcancel', this.onTouchCancel, true);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.m_elementData.touchCanvas.addEventListener('touchend', this.onTouchEnd, true);

this.m_keyboard = new Keyboard(window);

this.onKeyDown = this.onKeyDown.bind(this);
this.m_keyboard.on('keydown', this.onKeyDown);
this.onKeyUp = this.onKeyUp.bind(this);
this.m_keyboard.on('keyup', this.onKeyUp);
this.onKeyPress = this.onKeyPress.bind(this);
this.m_keyboard.on('keypress', this.onKeyPress);

this.m_keyboard.registerHotkey({ key: 'KeyD', shiftKey: true, ctrlKey: true }, async (event: KeyboardEvent) => {
    this.toggleFpsCounter();
    event.preventDefault();
});

this.onMouseWheel = this.onMouseWheel.bind(this);
this.m_elementData.touchCanvas.addEventListener('wheel', this.onMouseWheel, false);
this.m_elementData.touchCanvas.addEventListener('contextmenu', (event) => {
    event.preventDefault();
});

为了有序的分模块的响应鼠标事件,封装了MouseEventStragety类,它的一个实例被AppClient持有,绑定的事件都将送入到MouseEventStragety中。MouseEventStragety接受若干个用于响应具体事件的实例MouseEventStragetyIns。

注册实例对象,实例对象有优先级。

typescript
registerStragety(inst: MouseEventStragetyIns) {
        if (this.m_stragetyMap[inst.name]) {
            Trace.error('Twice Registered!');
            return;
        }

        this.m_stragetyMap[inst.name] = inst;
        this.m_priorityList.push(inst);
        this.m_priorityList = this.m_priorityList.sort((a, b) => {
            return a.priority < b.priority ? -1 : 1;
        });
    }

关注的响应策略函数是

  • onTouchStart
  • onTouchMove
  • onTouchEnd
  • onTouchCancel
  • onMouseWheel
  • onMouseDown
  • onMouseMove
  • onMouseUp
  • onMouseEnter
  • onMouseLeave

按照优先级响应事件,以mousedown为例,如果实例的onMouseDown为true,不再继续透传给其他实例。

typescript
onMouseDown(event: MouseEvent): boolean {
    event.preventDefault();
    this.m_mouseActFrameStamp = _GNowTimeMs;

    if (event.target != this.canvas) {
        return false;
    }

    this.m_mouseDownX = event.offsetX;
    this.m_mouseDownY = event.offsetY;
    this.m_shiftPress = event.shiftKey;

    for (const ctrl of this.m_priorityList) {
        if (ctrl.onMouseDown(event)) {
            break;
        }
    }

    return true;
}

目前已经内置了通用的响应实例,它们是

  • MESCameraCtrl: 把事件传递给相机控制器,用来控制相机
  • MESObjectSelect: 把事件向场景查询,用来拾取或选中物体
  • MESObjectCtrl: 把事件传递给物体控制器,用来控制物体的移动旋转和缩放

这三个控制策略每个只做一件事情,且它们之间也有优先级排序,如MESObjectCtrl要高于其他二者,在旋转物体的时候,就不要旋转相机镜头了。

插件或者说项目侧,还可以根据自身需要扩展自定义的鼠标响应策略,并注册到MouseEventStragety中。根据项目的需要选择性的注册需要的响应策略。

值得注意的是,AppClient及其派生类是可以通过override的方式重载方法来响应到鼠标事件的,但是从框架协同的角度来讲更期望使用MouseEventStragety来处理事件。另外的一个好处是,有的时候并不是仅仅有一个画布,可能右上角还有一个小视口来显示其他内容,那么针对画布的事件处理,可以复用同样的一套逻辑。

最后,我们可以看一下触摸的处理。触摸部分主要是封装了多个触摸点,在MouseEventStragety中,已经封装了数据结构,来描述和跟踪各个触点的down、move、up、cancle事件。后续可以方便的接入FingerGesture这种手势的插件。