src/components/InputHandler.js
import Script from './Script';
import System from '../systems/System';
const ControlDevice = {
NONE: 'none',
MOUSE_KEYBOARD: 'mouse-keyboard',
GAMEPAD: 'gamepad',
};
function getGamepadAxisById(axes, id) {
if (id === 'primary-x') {
return axes[0];
} else if (id === 'primary-y') {
return axes[1];
} else if (id === 'secondary-x') {
return axes[2];
} else if (id === 'secondary-y') {
return axes[3];
} else if (id === 'primary-left') {
return -Math.min(0, axes[0]);
} else if (id === 'primary-right') {
return Math.max(0, axes[0]);
} else if (id === 'primary-up') {
return -Math.min(0, axes[1]);
} else if (id === 'primary-down') {
return Math.max(0, axes[1]);
} else if (id === 'secondary-left') {
return -Math.min(0, axes[2]);
} else if (id === 'secondary-right') {
return Math.max(0, axes[2]);
} else if (id === 'secondary-up') {
return -Math.min(0, axes[3]);
} else if (id === 'secondary-down') {
return Math.max(0, axes[3]);
} else {
return 0;
}
}
/**
* Simple yet powerful input handler.
*
* @example
* const component = new InputHandler();
* component.deserialize({ requireGamepad: true });
*/
export default class InputHandler extends Script {
/**
* Component factory.
*
* @return {InputHandler} Component instance.
*/
static factory() {
return new InputHandler();
}
/** @type {*} */
static get propsTypes() {
return {
...Script.propsTypes,
requireGamepad: 'boolean',
acceptMouse: 'boolean',
acceptKeyboard: 'boolean',
acceptGamepad: 'boolean',
controlDeviceChangeTreshold: 'number',
repeatingTriggersDelay: 'number',
firstTriggersDelay: 'number',
acceptFirstConnectedGamepad: 'boolean'
};
}
/** @type {*} */
static get ControlDevice() {
return ControlDevice;
}
/** @type {boolean} */
get requireGamepad() {
return this._requireGamepad;
}
/** @type {boolean} */
set requireGamepad(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._requireGamepad = value;
}
/** @type {boolean} */
get acceptMouse() {
return this._acceptMouse;
}
/** @type {boolean} */
set acceptMouse(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._acceptMouse = value;
}
/** @type {boolean} */
get acceptKeyboard() {
return this._acceptKeyboard;
}
/** @type {boolean} */
set acceptKeyboard(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._acceptKeyboard = value;
}
/** @type {boolean} */
get acceptGamepad() {
return this._acceptGamepad;
}
/** @type {boolean} */
set acceptGamepad(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._acceptGamepad = value;
}
/** @type {number} */
get controlDeviceChangeTreshold() {
return this._controlDeviceChangeTreshold;
}
/** @type {number} */
set controlDeviceChangeTreshold(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._controlDeviceChangeTreshold = value;
}
/** @type {number} */
get repeatingTriggersDelay() {
return this._repeatingTriggersDelay;
}
/** @type {number} */
set repeatingTriggersDelay(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._repeatingTriggersDelay = value;
}
/** @type {number} */
get firstTriggersDelay() {
return this._firstTriggersDelay;
}
/** @type {number} */
set firstTriggersDelay(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number!');
}
this._firstTriggersDelay = value;
}
/** @type {boolean} */
get acceptFirstConnectedGamepad() {
return this._acceptFirstConnectedGamepad;
}
/** @type {boolean} */
set acceptFirstConnectedGamepad(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._acceptFirstConnectedGamepad = value;
}
/** @type {boolean} */
get isAcquiringGamepad() {
return !!this._acquireGamepad;
}
/** @type {*} */
get axes() {
const result = {};
for (const [key, value] of this._axes) {
result[key] = value;
}
return result;
}
/** @type {*} */
get triggers() {
const result = {};
for (const [key, value] of this._triggers) {
result[key] = value;
}
return result;
}
/** @type {number} */
get gamepadIndex() {
return this._gamepadIndex;
}
/** @type {string} */
get lastControlDevice() {
return this._lastControlDevice;
}
/**
* Constructor.
*/
constructor() {
super();
this._requireGamepad = false;
this._acceptMouse = true;
this._acceptKeyboard = true;
this._acceptGamepad = true;
this._controlDeviceChangeTreshold = 0.1;
this._repeatingTriggersDelay = -1;
this._firstTriggersDelay = -1;
this._acceptFirstConnectedGamepad = false;
this._configAxesMouse = new Map();
this._configAxesKey = new Map();
this._configAxesGamepad = new Map();
this._configAxesGamepadAxis = new Map();
this._configTriggersMouse = new Map();
this._configTriggersKey = new Map();
this._configTriggersGamepad = new Map();
this._configTriggersGamepadAxis = new Map();
this._axes = new Map();
this._axesPrev = new Map();
this._axesNext = new Map();
this._triggers = new Map();
this._triggersPrev = new Map();
this._triggersNext = new Map();
this._triggersTimers = new Map();
this._acquireGamepad = null;
this._gamepadIndex = -1;
this._lastControlDevice = ControlDevice.NONE;
}
/**
* @override
*/
dispose() {
this.clear();
this._acquireGamepad = null;
this._gamepadIndex = -1;
super.dispose();
}
/**
* Clear all axes, triggers and configs.
*
* @example
* component.clear();
*/
clear() {
this._configAxesMouse.clear();
this._configAxesKey.clear();
this._configAxesGamepad.clear();
this._configAxesGamepadAxis.clear();
this._configTriggersMouse.clear();
this._configTriggersKey.clear();
this._configTriggersGamepad.clear();
this._configTriggersGamepadAxis.clear();
this._axes.clear();
this._axesPrev.clear();
this._axesNext.clear();
this._triggers.clear();
this._triggersPrev.clear();
this._triggersNext.clear();
this._triggersTimers.clear();
}
/**
* Setup axes and triggers.
*
* @param {*} config - Configuration object.
*
* @example
* component.setup({ axes: { 'pos-x': { mouse: 'x' } }, triggers: { action: { key: 32 } } });
*/
setup(config) {
this.clear();
if (!config) {
return;
}
const { axes, triggers } = config;
if (!!axes) {
for (const name in axes) {
const axis = axes[name];
if (!axis) {
continue;
}
const {
mouse,
keys,
gamepad,
gamepadAxis,
} = axis;
if (typeof mouse === 'string') {
this._configAxesMouse.set(name, mouse);
}
if (keys instanceof Array && keys.length === 4) {
this._configAxesKey.set(
name,
keys.map(item => typeof item === 'string'
? item.charCodeAt(0)
: item
)
);
}
if (typeof gamepad === 'number') {
this._configAxesGamepad.set(name, gamepad);
}
if (typeof gamepadAxis === 'string') {
this._configAxesGamepadAxis.set(name, gamepadAxis);
}
}
}
if (!!triggers) {
for (const name in triggers) {
const trigger = triggers[name];
if (!trigger) {
continue;
}
const {
mouse,
key,
gamepad,
gamepadAxis,
} = trigger;
if (typeof mouse === 'number') {
this._configTriggersMouse.set(name, mouse);
} else if (typeof mouse === 'string') {
if (mouse === 'left') {
this._configTriggersMouse.set(name, 0);
} else if (mouse === 'middle') {
this._configTriggersMouse.set(name, 1);
} else if (mouse === 'right') {
this._configTriggersMouse.set(name, 2);
}
}
if (typeof key === 'number') {
this._configTriggersKey.set(name, key);
} else if (typeof key === 'string') {
this._configTriggersKey.set(name, key.charCodeAt(0));
}
if (typeof gamepad === 'number') {
this._configTriggersGamepad.set(name, gamepad);
}
if (typeof gamepadAxis === 'string') {
this._configTriggersGamepadAxis.set(name, gamepadAxis)
}
}
}
}
/**
* Get value of given axis.
*
* @param {string} id - Axis id.
* @param {number} treshold - Value treshold (if value is greater than treshold, return value, zero otherwise).
*
* @return {number} Axis value.
*
* @example
* x += component.getAxis('pos-x');
*/
getAxis(id, treshold = 0) {
const result = this._axes.get(id) || 0;
return Math.abs(result) > treshold ? result : 0;
}
/**
* Get delta value of given axis.
*
* @param {string} id - Axis id.
*
* @return {number} Axis value.
*
* @example
* if (component.getAxisDelta('pos-y') < 0) { entity.performAction('jump'); }
*/
getAxisDelta(id) {
return (this._axes.get(id) || 0) - (this._axesPrev.get(id) || 0);
}
/**
* Set given axis value.
*
* @param {string} id - Axis id.
* @param {number} value - New axis value.
*
* @example
* // reset axis to prevent further usage in this frame.
* component.setAxis('pos-x', 0);
*/
setAxis(id, value) {
this._axes.set(id, Math.max(-1, Math.min(1, value || 0)));
}
/**
* Tells if axis is currently hold.
*
* @param {string} id - Axis id.
* @param {number} treshold - Value treshold (if value is greater than treshold, return true, false otherwise).
*
* @return {boolean} Holding state.
*
* @example
* if (component.isAxisHold('pos-x')) { entity.performAction('animate', 'walk'); }
*/
isAxisHold(id, treshold = 0.5) {
return Math.abs(this.getAxis(id, treshold)) > 0;
}
/**
* Tells if axis is pressed in current frame.
*
* @param {string} id - Axis id.
* @param {number} treshold - Value treshold (if value is greater than treshold and axis was not hold in previous frame, return true, false otherwise).
*
* @return {boolean} Pressing state.
*
* @example
* if (component.isAxisPressed('bow')) { entity.performAction('aim'); }
*/
isAxisPressed(id, treshold = 0.5) {
const current = Math.abs(this._axes.get(id) || 0) > treshold;
const prev = Math.abs(this._axesPrev.get(id) || 0) > treshold;
return current && !prev;
}
/**
* Tells if axis is released in current frame.
*
* @param {string} id - Axis id.
* @param {number} treshold - Value treshold (if value is smaller than treshold and axis was hold in previous frame, return true, false otherwise).
*
* @return {boolean} Releasing state.
*
* @example
* if (component.isAxisReleased('bow')) { entity.performAction('fire'); }
*/
isAxisReleased(id, treshold = 0.5) {
const current = Math.abs(this._axes.get(id) || 0) > treshold;
const prev = Math.abs(this._axesPrev.get(id) || 0) > treshold;
return !current && prev;
}
/**
* Get value of given trigger.
*
* @param {string} id - Trigger id.
* @param {number} treshold - Value treshold (if value is greater than treshold, return value, zero otherwise).
*
* @return {number} Trigger value.
*
* @example
* speed += component.getTrigger('accel');
*/
getTrigger(id, treshold = 0) {
const result = this._triggers.get(id) || 0;
return result > treshold ? result : 0;
}
/**
* Get delta value of given trigger.
*
* @param {string} id - Trigger id.
*
* @return {number} Trigger value.
*
* @example
* if (component.getTriggerDelta('pull') > 0) { entity.performAction('pull-rope'); }
*/
getTriggerDelta(id) {
return (this._triggers.get(id) || 0) - (this._triggersPrev.get(id) || 0);
}
/**
* Set given trigger value.
*
* @param {string} id - Trigger id.
* @param {number} value - New trigger value.
*
* @example
* // reset trigger to prevent further usage in this frame.
* component.setTrigger('pos-x', 0);
*/
setTrigger(id, value) {
this._triggers.set(id, Math.max(0, Math.min(1, value || 0)));
}
/**
* Tells if trigger is currently hold.
*
* @param {string} id - Trigger id.
* @param {number} treshold - Value treshold (if value is greater than treshold, return true, false otherwise).
*
* @return {boolean} Holding state.
*
* @example
* if (component.isTriggerHold('swim')) { entity.performAction('swim'); }
*/
isTriggerHold(id, treshold = 0.5) {
return Math.abs(this.getTrigger(id, treshold)) > 0;
}
/**
* Tells if trigger is pressed in current frame.
*
* @param {string} id - Trigger id.
* @param {number} treshold - Value treshold (if value is greater than treshold and trigger was not hold in previous frame, return true, false otherwise).
*
* @return {boolean} Pressing state.
*
* @example
* if (component.isTriggerPressed('jump')) { entity.performAction('jump'); }
*/
isTriggerPressed(id, treshold = 0.5) {
const current = Math.abs(this._triggers.get(id) || 0) > treshold;
const prev = Math.abs(this._triggersPrev.get(id) || 0) > treshold;
return current && !prev;
}
/**
* Tells if trigger is released in current frame.
*
* @param {string} id - Trigger id.
* @param {number} treshold - Value treshold (if value is smaller than treshold and trigger was hold in previous frame, return true, false otherwise).
*
* @return {boolean} Releasing state.
*
* @example
* if (component.isTriggerReleased('fire')) { entity.performAction('fire'); }
*/
isTriggerReleased(id, treshold = 0.5) {
const current = Math.abs(this._triggers.get(id) || 0) > treshold;
const prev = Math.abs(this._triggersPrev.get(id) || 0) > treshold;
return !current && prev;
}
/**
* Asynchronously acquire gamepad (specify trigger to press and waiting duration).
*
* @param {number} trigger - Trigger index.
* @param {number} timeout - Waiting duration in milliseconds.
*
* @return {Promise} Waiting promise.
*
* @example
* component.acquireGamepad(0).then(() => System.events.trigger('player-is-ready'));
*/
acquireGamepad(trigger, timeout = 3000) {
if (typeof trigger !== 'number') {
throw new Error('`trigger` is not type of Number!');
}
if (typeof timeout !== 'number') {
throw new Error('`timeout` is not type of Number!');
}
if (trigger < 0) {
throw new Error('`trigger` cannot be less than 0!');
}
if (timeout <= 0) {
throw new Error('`timeout` must be grater than 0!');
}
if (!!this._acquireGamepad) {
throw new Error('Acquiring gamepad already in progress!');
}
this._gamepadIndex = -1;
return new Promise((resolve, reject) => {
const onTimeout = () => {
this._acquireGamepad = null;
this._gamepadIndex = -1;
reject(new Error('Detecting gamepad timeout!'));
};
const timer = setTimeout(onTimeout, timeout);
this._acquireGamepad = (value, index) => {
if (value !== trigger) {
return false;
}
this._acquireGamepad = null;
this._gamepadIndex = index;
clearTimeout(timer);
resolve();
return true;
};
});
}
/**
* Release acquired gamepad.
*
* @example
* if (gameOver) { component.releaseGamepad(); }
*/
releaseGamepad() {
this._gamepadIndex = -1;
}
/**
* Manually acquire gamepad by it's index.
*
* @param {number} index - Gamepad index.
*
* @example
* component.setAcquiredGamepad(0);
*/
setAcquiredGamepad(index) {
const { InputSystem } = System.systems;
if (!!InputSystem) {
for (const gamepad of InputSystem.gamepads.values()) {
if (index === gamepad.index) {
this._gamepadIndex = index;
return;
}
}
}
}
/**
* Get acquired gamepad instance.
*
* @return {Gamepad|null} Acquired gamepad instance or null.
*/
getAcquiredGamepad() {
const { InputSystem } = System.systems;
if (!!InputSystem) {
for (const gamepad of InputSystem.gamepads.values()) {
if (gamepad.index === this._gamepadIndex) {
return gamepad;
}
}
}
return null;
}
/**
* @override
*/
onAttach() {
this.listenTo = Script.EventFlags.INPUT;
super.onAttach();
}
/**
* @override
*/
onDetach() {
super.onDetach();
this.listenTo = Script.EventFlags.INPUT;
this.clear();
}
/**
* @override
*/
onUpdate(deltaTime) {
this._axesPrev.clear();
this._triggersPrev.clear();
for (const [key, value] of this._axes) {
this._axesPrev.set(key, value);
}
for (const [key, value] of this._triggers) {
this._triggersPrev.set(key, value);
}
for (const [key, value] of this._axesNext) {
this._axes.set(key, value);
}
for (const [key, value] of this._triggersNext) {
this._triggers.set(key, value);
}
this._axesNext.clear();
this._triggersNext.clear();
if (this._repeatingTriggersDelay > 0) {
for (let [key, value] of this._triggersTimers) {
value -= deltaTime;
this._triggersTimers.set(key, value);
if (value <= 0) {
this._triggers.delete(key);
this._triggersTimers.set(key, this._repeatingTriggersDelay);
}
}
}
}
/**
* @override
*/
onMouseDown(unitVec, screenVec, button) {
if (!this._acceptMouse) {
return;
}
this._applyControlDevice(ControlDevice.MOUSE_KEYBOARD);
for (const [key, value] of this._configTriggersMouse.entries()) {
if (value === button) {
this._triggersNext.set(key, 1);
if (this._firstTriggersDelay > 0) {
this._triggersTimers.set(key, this._firstTriggersDelay);
}
}
}
}
/**
* @override
*/
onMouseUp(unitVec, screenVec, button) {
if (!this._acceptMouse) {
return;
}
this._applyControlDevice(ControlDevice.MOUSE_KEYBOARD);
for (const [key, value] of this._configTriggersMouse.entries()) {
if (value === button) {
this._triggersNext.set(key, 0);
if (this._firstTriggersDelay > 0) {
this._triggersTimers.delete(key);
}
}
}
}
/**
* @override
*/
onMouseMove(unitVec, screenVec) {
if (!this._acceptMouse) {
return;
}
this._applyControlDevice(ControlDevice.MOUSE_KEYBOARD);
for (const [key, value] of this._configAxesMouse.entries()) {
if (value === 'x') {
this._axesNext.set(key, unitVec[0]);
} else if (value === 'y') {
this._axesNext.set(key, -unitVec[1]);
}
}
}
/**
* @override
*/
onKeyDown(code) {
if (!this._acceptKeyboard) {
return;
}
this._applyControlDevice(ControlDevice.MOUSE_KEYBOARD);
for (const [key, value] of this._configTriggersKey.entries()) {
if (value === code) {
this._triggersNext.set(key, 1);
if (this._firstTriggersDelay > 0) {
this._triggersTimers.set(key, this._firstTriggersDelay);
}
}
}
for (const [key, value] of this._configAxesKey.entries()) {
const [fc, fv, tc, tv] = value;
if (fc === code) {
this._axesNext.set(key, Math.max(-1, Math.min(1, fv)));
} else if (tc === code) {
this._axesNext.set(key, Math.max(-1, Math.min(1, tv)));
}
}
}
/**
* @override
*/
onKeyUp(code) {
if (!this._acceptKeyboard) {
return;
}
this._applyControlDevice(ControlDevice.MOUSE_KEYBOARD);
for (const [key, value] of this._configTriggersKey.entries()) {
if (value === code) {
this._triggersNext.set(key, 0);
if (this._firstTriggersDelay > 0) {
this._triggersTimers.delete(key);
}
}
}
for (const [key, value] of this._configAxesKey.entries()) {
const [fc, fv, tc, tv] = value;
if (fc === code) {
this._axesNext.set(key, 0);
} else if (tc === code) {
this._axesNext.set(key, 0);
}
}
}
/**
* @override
*/
onGamepadConnected(gamepad) {
if (!this._acceptGamepad) {
return;
}
if (this._gamepadIndex < 0 && this._acceptFirstConnectedGamepad) {
this._gamepadIndex = gamepad.index;
}
}
/**
* @override
*/
onGamepadDisconnected(gamepad) {
if (!this._acceptGamepad) {
return;
}
const {
_axes,
_axesPrev,
_axesNext,
_triggers,
_triggersPrev,
_triggersNext,
_triggersTimers
} = this;
const { index } = gamepad;
for (const key of [..._axes.keys()]) {
if (key.endsWith(`:${index}`)) {
_axes.delete(key);
_axesPrev.delete(key);
_axesNext.delete(key);
}
}
for (const key of [..._triggers.keys()]) {
if (key.endsWith(`:${index}`)) {
_triggers.delete(key);
_triggersPrev.delete(key);
_triggersNext.delete(key);
_triggersTimers.delete(key);
}
}
if (this._gamepadIndex === index) {
this._gamepadIndex = -1;
}
}
/**
* @override
*/
onGamepadProcess(gamepad) {
if (!this._acceptGamepad) {
return;
}
const { index, buttons, axes } = gamepad;
const {
_controlDeviceChangeTreshold,
_triggersTimers,
_firstTriggersDelay
} = this;
if (!!this._acquireGamepad) {
for (let i = 0, c = buttons.length; i < c; ++i) {
if (buttons[i].value > 0 && this._acquireGamepad(i, index)) {
break;
}
}
}
if (this._requireGamepad && this._gamepadIndex < 0) {
return;
}
for (const [key, value] of this._configTriggersGamepad.entries()) {
const button = buttons[value];
if (!!button) {
if (this._gamepadIndex < 0 || this._gamepadIndex === index) {
this._triggersNext.set(key, button.value || 0);
this._triggersNext.set(`${key}:${index}`, button.value || 0);
if (_firstTriggersDelay > 0) {
if (button.value || 0 > 0) {
if (!_triggersTimers.has(key)) {
_triggersTimers.set(key, _firstTriggersDelay);
_triggersTimers.set(`${key}:${index}`, _firstTriggersDelay);
}
} else {
_triggersTimers.delete(key);
_triggersTimers.delete(`${key}:${index}`);
}
}
if ((button.value || 0) > _controlDeviceChangeTreshold) {
this._applyControlDevice(ControlDevice.GAMEPAD);
}
}
}
}
for (const [key, value] of this._configTriggersGamepadAxis.entries()) {
const axis = getGamepadAxisById(axes, value);
if (this._gamepadIndex < 0 || this._gamepadIndex === index) {
this._triggersNext.set(key, axis || 0);
this._triggersNext.set(`${key}:${index}`, axis || 0);
if (_firstTriggersDelay > 0) {
if (Math.abs(axis || 0) > 0.5) {
if (!_triggersTimers.has(key)) {
_triggersTimers.set(key, _firstTriggersDelay);
_triggersTimers.set(`${key}:${index}`, _firstTriggersDelay);
}
} else {
_triggersTimers.delete(key);
_triggersTimers.delete(`${key}:${index}`);
}
}
if (Math.abs(axis || 0) > _controlDeviceChangeTreshold) {
this._applyControlDevice(ControlDevice.GAMEPAD);
}
}
}
for (const [key, value] of this._configAxesGamepad.entries()) {
const button = buttons[value];
if (!!button) {
if (this._gamepadIndex < 0 || this._gamepadIndex === index) {
this._axesNext.set(key, button.value || 0);
this._axesNext.set(`${key}:${index}`, button.value || 0);
if ((button.value || 0) > _controlDeviceChangeTreshold) {
this._applyControlDevice(ControlDevice.GAMEPAD);
}
}
}
}
for (const [key, value] of this._configAxesGamepadAxis.entries()) {
const axis = getGamepadAxisById(axes, value);
if (this._gamepadIndex < 0 || this._gamepadIndex === index) {
this._axesNext.set(key, axis || 0);
this._axesNext.set(`${key}:${index}`, axis || 0);
if (Math.abs(axis || 0) > _controlDeviceChangeTreshold) {
this._applyControlDevice(ControlDevice.GAMEPAD);
}
}
}
}
_applyControlDevice(device) {
const { _lastControlDevice } = this;
this._lastControlDevice = device;
if (_lastControlDevice !== device) {
System.events.trigger('input-handler-control-device', this, device);
}
}
}