美文网首页
(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) 用户输入检测

    用户输入检测 主线: 检测生命周期 初始化部分主要是调用硬件相关的接口, 绑定浏览器对应的事件 更新部分根据历史的...

  • 仿淘宝搜索框

    代码如下: 正常浏览器:检测用户输入状态:oninput IE678:检测用户输入状态:onpropertycha...

  • JavaScript高级程序设计笔记9

    客户端检测 能力检测 (1)更可靠的能力检测 (2)能力检测,不是浏览器检测 怪癖检测 用户代理检测 (1)用户代...

  • iOS实时检测UITextField内容 ,长度

    想在用户输入内容的时候同时检测UITextField的输入并根据用户的输入内容响应页面上的事件,在这个例子中是实时...

  • 解决UITextField中文输入长度不正确问题

    我们在用UITextField或者UITextView输入时,会碰到需要检测文本长度,或根据当前用户输入的文字执行...

  • 解决 GitHub 无法访问的问题

    修改hosts1.打开Dns检测|Dns查询 - 站长工具2.在检测输入栏中输入github.com3.把检测列表...

  • github无法访问解决

    修改hosts1.打开Dns检测|Dns查询 - 站长工具2.在检测输入栏中输入github.com官网3.把检测...

  • Python 输入与输出

    输入与输出 输入 python2 raw_input()将用户输入作为'str'赋值给变量input()将用户输入...

  • iOS UITextField,UITextView中英文混排长

    UITextField我们要在输入的时候检测用户输入的文字长度,当达到一定的限度的时候就限制输入了。首先我们添加一...

  • 4.1、购物车系统

    购物车系统 启动程序后,让用户输入工资,然后打印商品列表 允许用户根据商品编号购买商品 用户选择商品后,检测余额是...

网友评论

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

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