import { Observable } from "../Misc/observable"; import { Engine } from '../Engines/engine'; import { IDisposable } from '../scene'; import { Nullable } from '../types'; /** * This class will take all inputs from Keyboard, Pointer, and * any Gamepads and provide a polling system that all devices * will use. This class assumes that there will only be one * pointer device and one keyboard. */ export class DeviceInputSystem implements IDisposable { // Static /** POINTER_DEVICE */ public static readonly POINTER_DEVICE: string = "Pointer"; /** KEYBOARD_DEVICE */ public static readonly KEYBOARD_DEVICE: string = "Keyboard"; /** * Observable to be triggered when a device is connected */ public onDeviceConnectedObservable = new Observable(); /** * Observable to be triggered when a device is disconnected */ public onDeviceDisconnectedObservable = new Observable(); // Private Members private _inputs: { [key: string]: Array> } = {}; private _gamepads: Array; private _keyboardActive: boolean = false; private _pointerActive: boolean = false; private _elementToAttachTo: HTMLElement; private _keyboardDownEvent = (evt: any) => { }; private _keyboardUpEvent = (evt: any) => { }; private _pointerMoveEvent = (evt: any) => { }; private _pointerDownEvent = (evt: any) => { }; private _pointerUpEvent = (evt: any) => { }; private _gamepadConnectedEvent = (evt: any) => { }; private _gamepadDisconnectedEvent = (evt: any) => { }; private static _MAX_KEYCODES: number = 222; private static _MAX_POINTER_INPUTS: number = 7; /** * Default Constructor * @param engine - engine to pull input element from */ constructor(engine: Engine) { const inputElement = engine.getInputElement(); if (inputElement) { this._elementToAttachTo = inputElement; this._handleKeyActions(); this._handlePointerActions(); this._handleGamepadActions(); } } // Public functions /** * Checks for current device input value, given an id and input index * @param deviceName Id of connected device * @param inputIndex Index of device input * @returns Current value of input */ public pollInput(deviceName: string, inputIndex: number): Nullable { const device = this._inputs[deviceName]; if (!device) { throw `Unable to find device ${deviceName}`; } this._updateDevice(deviceName, inputIndex); if (device[inputIndex] === undefined) { throw `Unable to find input ${inputIndex} on device ${deviceName}`; } return device[inputIndex]; } /** * Dispose of all the eventlisteners and clears the observables */ public dispose() { this.onDeviceConnectedObservable.clear(); this.onDeviceDisconnectedObservable.clear(); // Keyboard Events if (this._keyboardActive) { window.removeEventListener("keydown", this._keyboardDownEvent); window.removeEventListener("keyup", this._keyboardUpEvent); } // Pointer Events if (this._pointerActive) { this._elementToAttachTo.removeEventListener("pointermove", this._pointerMoveEvent); this._elementToAttachTo.removeEventListener("pointerdown", this._pointerDownEvent); this._elementToAttachTo.removeEventListener("pointerup", this._pointerUpEvent); } // Gamepad Events window.removeEventListener("gamepadconnected", this._gamepadConnectedEvent); window.removeEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent); } // Private functions /** * Add device and inputs to device map * @param deviceName Assigned name of device (may be SN) * @param numberOfInputs Number of input entries to create for given device */ private _registerDevice(deviceName: string, numberOfInputs: number) { if (!this._inputs[deviceName]) { const device = new Array>(numberOfInputs); for (let i = 0; i < numberOfInputs; i++) { device[i] = null; } this._inputs[deviceName] = device; this.onDeviceConnectedObservable.notifyObservers(deviceName); } } /** * Given a specific device name, remove that device from the device map * @param deviceName Name of device to be removed */ private _unregisterDevice(deviceName: string) { if (this._inputs[deviceName]) { delete this._inputs[deviceName]; this.onDeviceDisconnectedObservable.notifyObservers(deviceName); } } /** * Handle all actions that come from keyboard interaction */ private _handleKeyActions() { this._keyboardDownEvent = ((evt) => { if (!this._keyboardActive) { this._keyboardActive = true; this._registerDevice(DeviceInputSystem.KEYBOARD_DEVICE, DeviceInputSystem._MAX_KEYCODES); } const kbKey = this._inputs[DeviceInputSystem.KEYBOARD_DEVICE]; if (kbKey) { kbKey[evt.keyCode] = 1; } }); this._keyboardUpEvent = ((evt) => { const kbKey = this._inputs[DeviceInputSystem.KEYBOARD_DEVICE]; if (kbKey) { kbKey[evt.keyCode] = 0; } }); window.addEventListener("keydown", this._keyboardDownEvent); window.addEventListener("keyup", this._keyboardUpEvent); } /** * Handle all actions that come from pointer interaction */ private _handlePointerActions() { this._pointerMoveEvent = ((evt) => { const deviceName = `${DeviceInputSystem.POINTER_DEVICE}-${evt.pointerId}`; if (!this._pointerActive) { this._pointerActive = true; this._registerDevice(deviceName, DeviceInputSystem._MAX_POINTER_INPUTS); } const pointer = this._inputs[deviceName]; if (pointer) { pointer[0] = evt.clientX; pointer[1] = evt.clientY; } }); this._pointerDownEvent = ((evt) => { const deviceName = `${DeviceInputSystem.POINTER_DEVICE}-${evt.pointerId}`; if (!this._pointerActive) { this._pointerActive = true; this._registerDevice(deviceName, DeviceInputSystem._MAX_POINTER_INPUTS); } const pointer = this._inputs[deviceName]; if (pointer) { pointer[0] = evt.clientX; pointer[1] = evt.clientY; pointer[evt.button + 2] = 1; } }); this._pointerUpEvent = ((evt) => { const deviceName = `${DeviceInputSystem.POINTER_DEVICE}-${evt.pointerId}`; const pointer = this._inputs[deviceName]; if (pointer) { pointer[evt.button + 2] = 0; } if (evt.pointerId != 1) // Don't unregister the mouse { this._unregisterDevice(deviceName); } }); this._elementToAttachTo.addEventListener("pointermove", this._pointerMoveEvent); this._elementToAttachTo.addEventListener("pointerdown", this._pointerDownEvent); this._elementToAttachTo.addEventListener("pointerup", this._pointerUpEvent); } /** * Handle all actions that come from gamepad interaction */ private _handleGamepadActions() { this._gamepadConnectedEvent = ((evt: any) => { const deviceName = `${evt.gamepad.id}-${evt.gamepad.index}`; this._registerDevice(deviceName, evt.gamepad.buttons.length + evt.gamepad.axes.length); this._gamepads = this._gamepads || new Array(evt.gamepad.index + 1); this._gamepads[evt.gamepad.index] = deviceName; }); this._gamepadDisconnectedEvent = ((evt: any) => { const deviceName = this._gamepads[evt.gamepad.index]; this._unregisterDevice(deviceName); delete this._gamepads[evt.gamepad.index]; }); window.addEventListener("gamepadconnected", this._gamepadConnectedEvent); window.addEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent); } /** * Update all non-event based devices with each frame */ private _updateDevice(deviceName: string, inputIndex: number) { // Gamepads const gamepads = navigator.getGamepads(); // Look for current gamepad and get updated values for (const gp of gamepads) { if (gp && deviceName == this._gamepads[gp.index]) { const device = this._inputs[deviceName]; if (inputIndex >= gp.buttons.length) { device[inputIndex] = gp.axes[inputIndex - gp.buttons.length].valueOf(); } else { device[inputIndex] = gp.buttons[inputIndex].value; } } } } }