美文网首页
(2) 用户输入检测

(2) 用户输入检测

作者: Heartcase_b8c0 | 来源:发表于2020-08-23 23:06 被阅读0次

    用户输入检测

    主线: 检测生命周期

    初始化部分
    主要是调用硬件相关的接口, 绑定浏览器对应的事件

    // 在场景管理器初始化输入, 稍微有点权责不明的感觉
    SceneManager.initialize = function() {
        this.initInput();
    };
    SceneManager.initInput = function() {
        Input.initialize();
        TouchInput.initialize();
    };
    

    更新部分
    根据历史的状态来推导如长按等状态

    // 由update更新输入状态信息
    // SceneManager.update 每tick更新一次, 每tick更新的帧数由fps决定
    SceneManager.updateMain = function() {
        this.updateInputData(); //
    };
    // 更新后的状态会在这一tick中被其他对象访问
    SceneManager.updateInputData = function() {
        Input.update();
        TouchInput.update();
    };
    

    主线 1: 键盘输入

    事件绑定

    Input.initialize = function() {
        this.clear(); // 初始化各种状态
        this._setupEventHandlers(); // 监听用户输入事件
    };
    // 分别是: 当用户按下键盘, 松开键盘和焦点离开页面的三种情况
    Input._setupEventHandlers = function() {
        document.addEventListener("keydown", this._onKeyDown.bind(this));
        document.addEventListener("keyup", this._onKeyUp.bind(this));
        window.addEventListener("blur", this._onLostFocus.bind(this));
    };
    
    //用户按下键盘
    Input._onKeyDown = function(event) {
        // 阻止键盘事件的默认行为
        if (this._shouldPreventDefault(event.keyCode)) {
            event.preventDefault();
        }
        // 按下小键盘按键能清空所有按键状态
        // 我也不知道有啥用...
        if (event.keyCode === 144) {
            // Numlock
            this.clear();
        }
        // KeyMap 部分见附录
        const buttonName = this.keyMapper[event.keyCode];
        if (buttonName) {
            // 设置按键当前状态
            this._currentState[buttonName] = true;
        }
    };
    
    // 用户松开按键
    Input._onKeyUp = function(event) {
        const buttonName = this.keyMapper[event.keyCode];
        if (buttonName) {
            // 取消按键状态
            this._currentState[buttonName] = false;
        }
    };
    
    // 页面失去焦点
    Input._onLostFocus = function() {
        // 清空所有按键状态
        this.clear();
    };
    

    主线 2: 手柄输入

    手柄输入和键盘输入的最大区别是,
    键盘的状态是由事件触发改变的, 记录在变量里, 在 update 循环的时候再被使用者读取
    而手柄的状态是在 update 循环时获取状态并记录在变量的
    二者的最后的按键状态是叠加的关系, 比如键盘按了左, 手柄按了右, 那就是左右都按下的状态
    事实上还有可能有多个手柄, 手柄和手柄之间的状态也是叠加的关系

    Input.update = function() {
        this._pollGamepads();
    };
    
    Input._pollGamepads = function() {
        // Web API, 用来获取当前可用的手柄
        if (navigator.getGamepads) {
            const gamepads = navigator.getGamepads();
            if (gamepads) {
                for (const gamepad of gamepads) {
                    if (gamepad && gamepad.connected) {
                        this._updateGamepadState(gamepad);
                    }
                }
            }
        }
    };
    
    Input._updateGamepadState = function(gamepad) {
        const lastState = this._gamepadStates[gamepad.index] || [];
        const newState = [];
        const buttons = gamepad.buttons; // 获取手柄按键
        const axes = gamepad.axes; // 获取手柄摇杆
        const threshold = 0.5;
        newState[12] = false;
        newState[13] = false;
        newState[14] = false;
        newState[15] = false;
        for (let i = 0; i < buttons.length; i++) {
            newState[i] = buttons[i].pressed; // 判断除摇杆以外其他按键是否被按下
        }
        // 判断摇杆方向
        if (axes[1] < -threshold) {
            newState[12] = true; // up
        } else if (axes[1] > threshold) {
            newState[13] = true; // down
        }
        if (axes[0] < -threshold) {
            newState[14] = true; // left
        } else if (axes[0] > threshold) {
            newState[15] = true; // right
        }
        // 这里判断是该摇杆的新状态是否和当前状态相比发生了改变
        // gamepadMapper的内容见附录
        for (let j = 0; j < newState.length; j++) {
            if (newState[j] !== lastState[j]) {
                const buttonName = this.gamepadMapper[j];
                if (buttonName) {
                    this._currentState[buttonName] = newState[j];
                }
            }
        }
        this._gamepadStates[gamepad.index] = newState;
    };
    

    主线: 键盘/手柄长按判断

    Input.update = function() {
        this._pollGamepads();
        // 长按计数器
        if (this._currentState[this._latestButton]) {
            this._pressedTime++;
        } else {
            this._latestButton = null;
        }
        // 对于每个新按下的按键, 重置长按计数器
        for (const name in this._currentState) {
            if (this._currentState[name] && !this._previousState[name]) {
                this._latestButton = name;
                this._pressedTime = 0;
                this._date = Date.now();
            }
            this._previousState[name] = this._currentState[name];
        }
        // 虚拟按键部分, 见后文
        if (this._virtualButton) {
            this._latestButton = this._virtualButton;
            this._pressedTime = 0;
            this._virtualButton = null;
        }
        // 更新方向输出
        this._updateDirection();
    };
    // 制作四方输出和八方输出
    // 虽然看上去很费力, 实际上四方只是mask了八方输出的结果而已
    // 四方输出的时候涉及到一个_preferredAxis
    // 它的逻辑是如果上一次y轴有变动, 那么当下一次x和y轴有变动时输出x, 反之输出y
    // 比如说我的角色正在向右行走, 按住右不放, 这时候按下上, 角色会向上走
    // 反过来, 如果角色正在向上行走, 按住上不放, 这时候按下左, 角色会向左走
    Input._updateDirection = function() {
        let x = this._signX();
        let y = this._signY();
        this._dir8 = this._makeNumpadDirection(x, y);
        if (x !== 0 && y !== 0) {
            if (this._preferredAxis === "x") {
                y = 0;
            } else {
                x = 0;
            }
        } else if (x !== 0) {
            this._preferredAxis = "y";
        } else if (y !== 0) {
            this._preferredAxis = "x";
        }
        this._dir4 = this._makeNumpadDirection(x, y);
    };
    // dir4和dir8的值
    Input._makeNumpadDirection = function(x, y) {
        if (x === 0 && y === 0) {
            return 0;
        } else {
            return 5 - y * 3 + x;
        }
    };
    // 也就是:
    // 1 2 3
    // 4 0 6
    // 7 8 9
    

    支线: 虚拟按键

    虚拟按键的使用场景是在 Sprite_Button 中,
    通过调用 virtualClick 来设置_virtualButton 的状态
    然后这个值会在下一个 tick 被 update 函数读取

    Input.virtualClick = function(buttonName) {
        this._virtualButton = buttonName;
    };
    
    Sprite_Button.prototype.onClick = function() {
        if (this._clickHandler) {
            // 非虚拟按键的回调
            this._clickHandler();
        } else {
            // 虚拟案件回调
            Input.virtualClick(this._buttonType);
        }
    };
    
    // 在Sprit_Button的update中, 通过判断TouchInput状态来触发onClick
    Sprite_Clickable.prototype.processTouch = function() {
        if (this.isClickEnabled()) {
            if (this.isBeingTouched()) {
                if (!this._hovered && TouchInput.isHovered()) {
                    this._hovered = true;
                    this.onMouseEnter();
                }
                if (TouchInput.isTriggered()) {
                    this._pressed = true;
                    this.onPress();
                }
            } else {
                if (this._hovered) {
                    this.onMouseExit();
                }
                this._pressed = false;
                this._hovered = false;
            }
            if (this._pressed && TouchInput.isReleased()) {
                this._pressed = false;
                this.onClick(); // 这里
            }
        } else {
            this._pressed = false;
            this._hovered = false;
        }
    };
    
    // 想设置一个虚拟按键只需要传入一个buttonType类型就可以了
    Sprite_Button.prototype.initialize = function(buttonType) {
        Sprite_Clickable.prototype.initialize.call(this);
        this._buttonType = buttonType;
        this._clickHandler = null;
        this._coldFrame = null;
        this._hotFrame = null;
        this.setupFrames();
    };
    // 比如
    this._menuButton = new Sprite_Button("menu");
    

    主线 3: 触控/鼠标输入

    和键盘一样, 触控/鼠标也是通过事件来改变状态的

    TouchInput.initialize = function() {
        this.clear();
        this._setupEventHandlers();
    };
    
    TouchInput._setupEventHandlers = function() {
        // 允许事件执行preventDefault
        const pf = { passive: false };
        document.addEventListener("mousedown", this._onMouseDown.bind(this));
        document.addEventListener("mousemove", this._onMouseMove.bind(this));
        document.addEventListener("mouseup", this._onMouseUp.bind(this));
        document.addEventListener("wheel", this._onWheel.bind(this), pf);
        document.addEventListener("touchstart", this._onTouchStart.bind(this), pf);
        document.addEventListener("touchmove", this._onTouchMove.bind(this), pf);
        document.addEventListener("touchend", this._onTouchEnd.bind(this));
        document.addEventListener("touchcancel", this._onTouchCancel.bind(this));
        window.addEventListener("blur", this._onLostFocus.bind(this));
    };
    
    // 最基础的鼠标左/右键的监控
    TouchInput._onMouseDown = function(event) {
        if (event.button === 0) {
            this._onLeftButtonDown(event); // 左键
        } else if (event.button === 1) {
            this._onMiddleButtonDown(event); // 无行为
        } else if (event.button === 2) {
            this._onRightButtonDown(event); // 右键
        }
    };
    
    // 左键的行为
    TouchInput._onLeftButtonDown = function(event) {
        // 获取鼠标相对于画布的坐标
        const x = Graphics.pageToCanvasX(event.pageX);
        const y = Graphics.pageToCanvasY(event.pageY);
        // 判断是否在画布中
        if (Graphics.isInsideCanvas(x, y)) {
            // 设置鼠标状态, 初始化长按计数器,
            this._mousePressed = true;
            this._pressedTime = 0;
            // 记录鼠标的xy坐标
            // 顺便说一句这个函数还记录了一个_date的属性
            // 记录的是每次按下鼠标的日期
            // 但是这个属性和它对外暴露的属性date, 都没有被引用
            // 这也许是以后feature的坑, 也许是被删掉的feature的残骸?
            this._onTrigger(x, y);
        }
    };
    TouchInput._onTrigger = function(x, y) {
        this._newState.triggered = true;
        this._x = x;
        this._y = y;
        this._triggerX = x;
        this._triggerY = y;
        this._moved = false;
        this._date = Date.now();
    };
    
    // 右键的行为
    // 基本上的逻辑和左键类似, 只不过是设置为了取消的状态
    TouchInput._onRightButtonDown = function(event) {
        const x = Graphics.pageToCanvasX(event.pageX);
        const y = Graphics.pageToCanvasY(event.pageY);
        if (Graphics.isInsideCanvas(x, y)) {
            this._onCancel(x, y);
        }
    };
    TouchInput._onCancel = function(x, y) {
        this._newState.cancelled = true;
        this._x = x;
        this._y = y;
    };
    
    // 鼠标移动的行为, 类似的逻辑
    // 我觉得这个地方其实可以抽出来计算坐标的逻辑
    // 这个代码不够Dry啊
    TouchInput._onMouseMove = function(event) {
        const x = Graphics.pageToCanvasX(event.pageX);
        const y = Graphics.pageToCanvasY(event.pageY);
        if (this._mousePressed) {
            this._onMove(x, y);
        } else if (Graphics.isInsideCanvas(x, y)) {
            this._onHover(x, y);
        }
    };
    // 按下的状态移动, 实际上感觉就是拖拽
    TouchInput._onMove = function(x, y) {
        // 判断从起始拖拽的点的相对位置
        const dx = Math.abs(x - this._triggerX);
        const dy = Math.abs(y - this._triggerY);
        if (dx > this.moveThreshold || dy > this.moveThreshold) {
            this._moved = true;
        }
        // 如果超过阈值, 则判断进行了拖拽
        if (this._moved) {
            this._newState.moved = true;
            this._x = x;
            this._y = y;
        }
    };
    // 这个只会在鼠标模式下生效, 鼠标滑过但是没有按下
    TouchInput._onHover = function(x, y) {
        this._newState.hovered = true;
        this._x = x;
        this._y = y;
    };
    // 当左键松开时...
    TouchInput._onMouseUp = function(event) {
        if (event.button === 0) {
            const x = Graphics.pageToCanvasX(event.pageX);
            const y = Graphics.pageToCanvasY(event.pageY);
            this._mousePressed = false;
            this._onRelease(x, y);
        }
    };
    TouchInput._onRelease = function(x, y) {
        this._newState.released = true;
        this._x = x;
        this._y = y;
    };
    // 当滑动滚轮时, 最后的值会是每一tick内所有事件的deltaX, deltaY的总和
    TouchInput._onWheel = function(event) {
        this._newState.wheelX += event.deltaX;
        this._newState.wheelY += event.deltaY;
        event.preventDefault();
    };
    // 在有新的触摸点的时候触发
    TouchInput._onTouchStart = function(event) {
        // changedTouches 距上次一新的触摸点的数组
        for (const touch of event.changedTouches) {
            const x = Graphics.pageToCanvasX(touch.pageX);
            const y = Graphics.pageToCanvasY(touch.pageY);
            if (Graphics.isInsideCanvas(x, y)) {
                this._screenPressed = true;
                this._pressedTime = 0;
                // touches代表所有触摸点
                // 当有2个以上触摸点时, 相当于鼠标右键
                // 否则相当于鼠标左键
                if (event.touches.length >= 2) {
                    this._onCancel(x, y);
                } else {
                    this._onTrigger(x, y);
                }
                event.preventDefault();
            }
        }
        if (window.cordova || window.navigator.standalone) {
            event.preventDefault();
        }
    };
    
    // 当发生触摸拖拽的时触发
    TouchInput._onTouchMove = function(event) {
        for (const touch of event.changedTouches) {
            const x = Graphics.pageToCanvasX(touch.pageX);
            const y = Graphics.pageToCanvasY(touch.pageY);
            this._onMove(x, y);
        }
    };
    
    // 当触摸结束时触发
    TouchInput._onTouchEnd = function(event) {
        for (const touch of event.changedTouches) {
            const x = Graphics.pageToCanvasX(touch.pageX);
            const y = Graphics.pageToCanvasY(touch.pageY);
            this._screenPressed = false;
            this._onRelease(x, y);
        }
    };
    
    // 触摸点被中断时会触发 (比如创建了过多的触摸点)
    TouchInput._onTouchCancel = function(/*event*/) {
        this._screenPressed = false;
    };
    
    // 失去焦点
    TouchInput._onLostFocus = function() {
        this.clear();
    };
    
    

    主线: 鼠标/触摸长按

    和 Input 的逻辑是类似的, 记录上一帧的鼠标状态,来记录长按状态

    TouchInput.update = function() {
        this._currentState = this._newState;
        this._newState = this._createNewState();
        this._clicked = this._currentState.released && !this._moved;
        if (this.isPressed()) {
            this._pressedTime++;
        }
    };
    

    主线: 状态的输出

    最后这么一大堆代码, 输出的内容其实并不多
    都是根据按下的键的状态和长按状态来做判断

    Input.isLongPressed // 长按
    Input.dir4 // 四方向输出
    Input.dir8 // 八方向输出
    Input.isPressed // 按下(未释放)
    Input.isRepeated // 长按并重复触发(比如每24帧触发一次)
    Input.isTriggered // 按下(第一帧)
    TouchInput.isCancelled
    TouchInput.isMoved
    TouchInput.isReleased
    TouchInput.isHovered
    TouchInput.isLongPressed
    TouchInput.isRepeated
    TouchInput.isClicked
    TouchInput.wheelX
    TouchInput.wheelY
    TouchInput.x
    TouchInput.y
    

    附录: 隐藏右键菜单

    你知道么, RMMZ 是用这种方式来组织鼠标右键默认调出菜单的操作的

    Graphics._disableContextMenu = function() {
        const elements = document.body.getElementsByTagName("*");
        const oncontextmenu = () => false;
        for (const element of elements) {
            element.oncontextmenu = oncontextmenu;
        }
    };
    

    附录: 键值表

    Input.keyMapper = {
        9: "tab", // tab
        13: "ok", // enter
        16: "shift", // shift
        17: "control", // control
        18: "control", // alt
        27: "escape", // escape
        32: "ok", // space
        33: "pageup", // pageup
        34: "pagedown", // pagedown
        37: "left", // left arrow
        38: "up", // up arrow
        39: "right", // right arrow
        40: "down", // down arrow
        45: "escape", // insert
        81: "pageup", // Q
        87: "pagedown", // W
        88: "escape", // X
        90: "ok", // Z
        96: "escape", // numpad 0
        98: "down", // numpad 2
        100: "left", // numpad 4
        102: "right", // numpad 6
        104: "up", // numpad 8
        120: "debug" // F9
    };
    
    Input.gamepadMapper = {
        0: "ok", // A
        1: "cancel", // B
        2: "shift", // X
        3: "menu", // Y
        4: "pageup", // LB
        5: "pagedown", // RB
        12: "up", // D-pad up
        13: "down", // D-pad down
        14: "left", // D-pad left
        15: "right" // D-pad right
    };
    

    总结

    image.png

    相关文章

      网友评论

          本文标题:(2) 用户输入检测

          本文链接:https://www.haomeiwen.com/subject/ijxgjktx.html