用户输入检测
主线: 检测生命周期
初始化部分
主要是调用硬件相关的接口, 绑定浏览器对应的事件
// 在场景管理器初始化输入, 稍微有点权责不明的感觉
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
};
网友评论