deviceInputSystem.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import { Engine } from '../Engines/engine';
  2. import { IDisposable } from '../scene';
  3. import { Nullable } from '../types';
  4. import { DeviceType } from './InputDevices/deviceEnums';
  5. /** @hidden */
  6. declare const _native: any;
  7. /**
  8. * This class will take all inputs from Keyboard, Pointer, and
  9. * any Gamepads and provide a polling system that all devices
  10. * will use. This class assumes that there will only be one
  11. * pointer device and one keyboard.
  12. */
  13. export class DeviceInputSystem implements IDisposable {
  14. /**
  15. * Returns onDeviceConnected callback property
  16. * @returns Callback with function to execute when a device is connected
  17. */
  18. public get onDeviceConnected() { return this._onDeviceConnected; }
  19. /**
  20. * Sets callback function when a device is connected and executes against all connected devices
  21. * @param callback Function to execute when a device is connected
  22. */
  23. public set onDeviceConnected(callback) {
  24. this._onDeviceConnected = callback;
  25. // Iterate through each active device and rerun new callback
  26. for (let deviceType = 0; deviceType < this._inputs.length; deviceType++) {
  27. if (this._inputs[deviceType]) {
  28. for (let deviceSlot = 0; deviceSlot < this._inputs[deviceType].length; deviceSlot++) {
  29. if (this._inputs[deviceType][deviceSlot]) {
  30. this._onDeviceConnected(deviceType, deviceSlot);
  31. }
  32. }
  33. }
  34. }
  35. }
  36. /**
  37. * Callback to be triggered when a device is disconnected
  38. */
  39. public onDeviceDisconnected: (deviceType: DeviceType, deviceSlot: number) => void = () => { };
  40. /**
  41. * Callback to be triggered when event driven input is updated
  42. */
  43. public onInputChanged: (deviceType: DeviceType, deviceSlot: number, inputIndex: number, previousState: Nullable<number>, currentState: Nullable<number>) => void;
  44. // Private Members
  45. private _inputs: Array<Array<Array<number>>> = [];
  46. private _gamepads: Array<DeviceType>;
  47. private _keyboardActive: boolean = false;
  48. private _pointerActive: boolean = false;
  49. private _elementToAttachTo: HTMLElement;
  50. private _keyboardDownEvent = (evt: any) => { };
  51. private _keyboardUpEvent = (evt: any) => { };
  52. private _pointerMoveEvent = (evt: any) => { };
  53. private _pointerDownEvent = (evt: any) => { };
  54. private _pointerUpEvent = (evt: any) => { };
  55. private _gamepadConnectedEvent = (evt: any) => { };
  56. private _gamepadDisconnectedEvent = (evt: any) => { };
  57. private _onDeviceConnected: (deviceType: DeviceType, deviceSlot: number) => void = () => { };
  58. private static _MAX_KEYCODES: number = 255;
  59. private static _MAX_POINTER_INPUTS: number = 7;
  60. private constructor(engine: Engine) {
  61. const inputElement = engine.getInputElement();
  62. if (inputElement) {
  63. this._elementToAttachTo = inputElement;
  64. this._handleKeyActions();
  65. this._handlePointerActions();
  66. this._handleGamepadActions();
  67. // Check for devices that are already connected but aren't registered. Currently, only checks for gamepads and mouse
  68. this._checkForConnectedDevices();
  69. }
  70. }
  71. /**
  72. * Creates a new DeviceInputSystem instance
  73. * @param engine Engine to pull input element from
  74. * @returns The new instance
  75. */
  76. public static Create(engine: Engine): DeviceInputSystem {
  77. // If running in Babylon Native, then defer to the native input system, which has the same public contract
  78. if (typeof _native !== 'undefined' && _native.DeviceInputSystem) {
  79. return new _native.DeviceInputSystem(engine);
  80. }
  81. return new DeviceInputSystem(engine);
  82. }
  83. // Public functions
  84. /**
  85. * Checks for current device input value, given an id and input index
  86. * @param deviceName Id of connected device
  87. * @param inputIndex Index of device input
  88. * @returns Current value of input
  89. */
  90. /**
  91. * Checks for current device input value, given an id and input index. Throws exception if requested device not initialized.
  92. * @param deviceType Enum specifiying device type
  93. * @param deviceSlot "Slot" or index that device is referenced in
  94. * @param inputIndex Id of input to be checked
  95. * @returns Current value of input
  96. */
  97. public pollInput(deviceType: DeviceType, deviceSlot: number, inputIndex: number): number {
  98. const device = this._inputs[deviceType][deviceSlot];
  99. if (!device) {
  100. throw `Unable to find device ${DeviceType[deviceType]}`;
  101. }
  102. this._updateDevice(deviceType, deviceSlot, inputIndex);
  103. if (device[inputIndex] === undefined) {
  104. throw `Unable to find input ${inputIndex} for device ${DeviceType[deviceType]} in slot ${deviceSlot}`;
  105. }
  106. return device[inputIndex];
  107. }
  108. /**
  109. * Dispose of all the eventlisteners
  110. */
  111. public dispose() {
  112. // Keyboard Events
  113. if (this._keyboardActive) {
  114. window.removeEventListener("keydown", this._keyboardDownEvent);
  115. window.removeEventListener("keyup", this._keyboardUpEvent);
  116. }
  117. // Pointer Events
  118. if (this._pointerActive) {
  119. this._elementToAttachTo.removeEventListener("pointermove", this._pointerMoveEvent);
  120. this._elementToAttachTo.removeEventListener("pointerdown", this._pointerDownEvent);
  121. this._elementToAttachTo.removeEventListener("pointerup", this._pointerUpEvent);
  122. }
  123. // Gamepad Events
  124. window.removeEventListener("gamepadconnected", this._gamepadConnectedEvent);
  125. window.removeEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  126. }
  127. /**
  128. * Checks for existing connections to devices and register them, if necessary
  129. * Currently handles gamepads and mouse
  130. */
  131. private _checkForConnectedDevices() {
  132. const gamepads = navigator.getGamepads();
  133. for (const gamepad of gamepads) {
  134. if (gamepad) {
  135. this._addGamePad(gamepad);
  136. }
  137. }
  138. // If the device in use has mouse capabilities, pre-register mouse
  139. if (matchMedia('(pointer:fine)').matches) {
  140. // This will provide a dummy value for the cursor position and is expected to be overriden when the first mouse event happens.
  141. // There isn't any good way to get the current position outside of a pointer event so that's why this was done.
  142. this._addPointerDevice(DeviceType.Mouse, 0, 0, 0);
  143. }
  144. }
  145. // Private functions
  146. /**
  147. * Add a gamepad to the DeviceInputSystem
  148. * @param gamepad A single DOM Gamepad object
  149. */
  150. private _addGamePad(gamepad: any) {
  151. const deviceType = this._getGamepadDeviceType(gamepad.id);
  152. const deviceSlot = gamepad.index;
  153. this._registerDevice(deviceType, deviceSlot, gamepad.buttons.length + gamepad.axes.length);
  154. this._gamepads = this._gamepads || new Array<DeviceType>(gamepad.index + 1);
  155. this._gamepads[deviceSlot] = deviceType;
  156. }
  157. /**
  158. * Add pointer device to DeviceInputSystem
  159. * @param deviceType Type of Pointer to add
  160. * @param deviceSlot Pointer ID (0 for mouse, pointerId for Touch)
  161. * @param currentX Current X at point of adding
  162. * @param currentY Current Y at point of adding
  163. */
  164. private _addPointerDevice(deviceType: DeviceType, deviceSlot: number, currentX: number, currentY: number) {
  165. this._pointerActive = true;
  166. this._registerDevice(deviceType, deviceSlot, DeviceInputSystem._MAX_POINTER_INPUTS);
  167. const pointer = this._inputs[deviceType][deviceSlot]; /* initalize our pointer position immediately after registration */
  168. pointer[0] = currentX;
  169. pointer[1] = currentY;
  170. }
  171. /**
  172. * Add device and inputs to device array
  173. * @param deviceType Enum specifiying device type
  174. * @param deviceSlot "Slot" or index that device is referenced in
  175. * @param numberOfInputs Number of input entries to create for given device
  176. */
  177. private _registerDevice(deviceType: DeviceType, deviceSlot: number, numberOfInputs: number) {
  178. if (!this._inputs[deviceType]) {
  179. this._inputs[deviceType] = [];
  180. }
  181. if (!this._inputs[deviceType][deviceSlot]) {
  182. const device = new Array<number>(numberOfInputs);
  183. for (let i = 0; i < numberOfInputs; i++) {
  184. device[i] = 0; /* set device input as unpressed */
  185. }
  186. this._inputs[deviceType][deviceSlot] = device;
  187. this.onDeviceConnected(deviceType, deviceSlot);
  188. }
  189. }
  190. /**
  191. * Given a specific device name, remove that device from the device map
  192. * @param deviceType Enum specifiying device type
  193. * @param deviceSlot "Slot" or index that device is referenced in
  194. */
  195. private _unregisterDevice(deviceType: DeviceType, deviceSlot: number) {
  196. if (this._inputs[deviceType][deviceSlot]) {
  197. delete this._inputs[deviceType][deviceSlot];
  198. this.onDeviceDisconnected(deviceType, deviceSlot);
  199. }
  200. }
  201. /**
  202. * Handle all actions that come from keyboard interaction
  203. */
  204. private _handleKeyActions() {
  205. this._keyboardDownEvent = ((evt) => {
  206. if (!this._keyboardActive) {
  207. this._keyboardActive = true;
  208. this._registerDevice(DeviceType.Keyboard, 0, DeviceInputSystem._MAX_KEYCODES);
  209. }
  210. const kbKey = this._inputs[DeviceType.Keyboard][0];
  211. if (kbKey) {
  212. if (this.onInputChanged) {
  213. this.onInputChanged(DeviceType.Keyboard, 0, evt.keyCode, kbKey[evt.keyCode], 1);
  214. }
  215. kbKey[evt.keyCode] = 1;
  216. }
  217. });
  218. this._keyboardUpEvent = ((evt) => {
  219. const kbKey = this._inputs[DeviceType.Keyboard][0];
  220. if (kbKey) {
  221. if (this.onInputChanged) {
  222. this.onInputChanged(DeviceType.Keyboard, 0, evt.keyCode, kbKey[evt.keyCode], 0);
  223. }
  224. kbKey[evt.keyCode] = 0;
  225. }
  226. });
  227. window.addEventListener("keydown", this._keyboardDownEvent);
  228. window.addEventListener("keyup", this._keyboardUpEvent);
  229. }
  230. /**
  231. * Handle all actions that come from pointer interaction
  232. */
  233. private _handlePointerActions() {
  234. this._pointerMoveEvent = ((evt) => {
  235. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  236. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  237. if (!this._inputs[deviceType]) {
  238. this._inputs[deviceType] = [];
  239. }
  240. if (!this._inputs[deviceType][deviceSlot]) {
  241. this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
  242. }
  243. const pointer = this._inputs[deviceType][deviceSlot];
  244. if (pointer) {
  245. if (this.onInputChanged) {
  246. this.onInputChanged(deviceType, deviceSlot, 0, pointer[0], evt.clientX);
  247. this.onInputChanged(deviceType, deviceSlot, 1, pointer[1], evt.clientY);
  248. }
  249. pointer[0] = evt.clientX;
  250. pointer[1] = evt.clientY;
  251. }
  252. });
  253. this._pointerDownEvent = ((evt) => {
  254. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  255. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  256. if (!this._inputs[deviceType]) {
  257. this._inputs[deviceType] = [];
  258. }
  259. if (!this._inputs[deviceType][deviceSlot]) {
  260. this._addPointerDevice(deviceType, deviceSlot, evt.clientX, evt.clientY);
  261. }
  262. const pointer = this._inputs[deviceType][deviceSlot];
  263. if (pointer) {
  264. if (this.onInputChanged) {
  265. this.onInputChanged(deviceType, deviceSlot, 0, pointer[0], evt.clientX);
  266. this.onInputChanged(deviceType, deviceSlot, 1, pointer[1], evt.clientY);
  267. this.onInputChanged(deviceType, deviceSlot, evt.button + 2, pointer[evt.button + 2], 1);
  268. }
  269. pointer[0] = evt.clientX;
  270. pointer[1] = evt.clientY;
  271. pointer[evt.button + 2] = 1;
  272. }
  273. });
  274. this._pointerUpEvent = ((evt) => {
  275. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  276. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  277. const pointer = this._inputs[deviceType][deviceSlot];
  278. if (pointer) {
  279. if (this.onInputChanged) {
  280. this.onInputChanged(deviceType, deviceSlot, evt.button + 2, pointer[evt.button + 2], 0);
  281. }
  282. pointer[0] = evt.clientX;
  283. pointer[1] = evt.clientY;
  284. pointer[evt.button + 2] = 0;
  285. }
  286. // We don't want to unregister the mouse because we may miss input data when a mouse is moving after a click
  287. if (evt.pointerType != "mouse") {
  288. this._unregisterDevice(deviceType, deviceSlot);
  289. }
  290. });
  291. this._elementToAttachTo.addEventListener("pointermove", this._pointerMoveEvent);
  292. this._elementToAttachTo.addEventListener("pointerdown", this._pointerDownEvent);
  293. this._elementToAttachTo.addEventListener("pointerup", this._pointerUpEvent);
  294. }
  295. /**
  296. * Handle all actions that come from gamepad interaction
  297. */
  298. private _handleGamepadActions() {
  299. this._gamepadConnectedEvent = ((evt: any) => {
  300. this._addGamePad(evt.gamepad);
  301. });
  302. this._gamepadDisconnectedEvent = ((evt: any) => {
  303. if (this._gamepads) {
  304. const deviceType = this._getGamepadDeviceType(evt.gamepad.id);
  305. const deviceSlot = evt.gamepad.index;
  306. this._unregisterDevice(deviceType, deviceSlot);
  307. delete this._gamepads[deviceSlot];
  308. }
  309. });
  310. window.addEventListener("gamepadconnected", this._gamepadConnectedEvent);
  311. window.addEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  312. }
  313. /**
  314. * Update all non-event based devices with each frame
  315. * @param deviceType Enum specifiying device type
  316. * @param deviceSlot "Slot" or index that device is referenced in
  317. * @param inputIndex Id of input to be checked
  318. */
  319. private _updateDevice(deviceType: DeviceType, deviceSlot: number, inputIndex: number) {
  320. // Gamepads
  321. const gp = navigator.getGamepads()[deviceSlot];
  322. if (gp && deviceType == this._gamepads[deviceSlot]) {
  323. const device = this._inputs[deviceType][deviceSlot];
  324. if (inputIndex >= gp.buttons.length) {
  325. device[inputIndex] = gp.axes[inputIndex - gp.buttons.length].valueOf();
  326. }
  327. else {
  328. device[inputIndex] = gp.buttons[inputIndex].value;
  329. }
  330. }
  331. }
  332. /**
  333. * Gets DeviceType from the device name
  334. * @param deviceName Name of Device from DeviceInputSystem
  335. * @returns DeviceType enum value
  336. */
  337. private _getGamepadDeviceType(deviceName: string): DeviceType {
  338. if (deviceName.indexOf("054c") !== -1 && deviceName.indexOf("0ce6") === -1) { // DualShock 4 Gamepad
  339. return DeviceType.DualShock;
  340. }
  341. else if (deviceName.indexOf("Xbox One") !== -1 || deviceName.search("Xbox 360") !== -1 || deviceName.search("xinput") !== -1) { // Xbox Gamepad
  342. return DeviceType.Xbox;
  343. }
  344. else if (deviceName.indexOf("057e") !== -1) { // Switch Gamepad
  345. return DeviceType.Switch;
  346. }
  347. return DeviceType.Generic;
  348. }
  349. }