deviceInputSystem.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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. * Callback to be triggered when a device is connected
  16. */
  17. public onDeviceConnected: (deviceType: DeviceType, deviceSlot: number) => void = () => { };
  18. /**
  19. * Callback to be triggered when a device is disconnected
  20. */
  21. public onDeviceDisconnected: (deviceType: DeviceType, deviceSlot: number) => void = () => { };
  22. /**
  23. * Callback to be triggered when event driven input is updated
  24. */
  25. public onInputChanged: (deviceType: DeviceType, deviceSlot: number, inputIndex: number, previousState: Nullable<number>, currentState: Nullable<number>) => void;
  26. // Private Members
  27. private _inputs: Array<Array<Array<number>>> = [];
  28. private _gamepads: Array<DeviceType>;
  29. private _keyboardActive: boolean = false;
  30. private _pointerActive: boolean = false;
  31. private _elementToAttachTo: HTMLElement;
  32. private _keyboardDownEvent = (evt: any) => { };
  33. private _keyboardUpEvent = (evt: any) => { };
  34. private _pointerMoveEvent = (evt: any) => { };
  35. private _pointerDownEvent = (evt: any) => { };
  36. private _pointerUpEvent = (evt: any) => { };
  37. private _gamepadConnectedEvent = (evt: any) => { };
  38. private _gamepadDisconnectedEvent = (evt: any) => { };
  39. private static _MAX_KEYCODES: number = 255;
  40. private static _MAX_POINTER_INPUTS: number = 7;
  41. private constructor(engine: Engine) {
  42. const inputElement = engine.getInputElement();
  43. if (inputElement) {
  44. this._elementToAttachTo = inputElement;
  45. this._handleKeyActions();
  46. this._handlePointerActions();
  47. this._handleGamepadActions();
  48. }
  49. }
  50. /**
  51. * Creates a new DeviceInputSystem instance
  52. * @param engine Engine to pull input element from
  53. * @returns The new instance
  54. */
  55. public static Create(engine: Engine): DeviceInputSystem {
  56. // If running in Babylon Native, then defer to the native input system, which has the same public contract
  57. if (typeof _native !== 'undefined' && _native.DeviceInputSystem) {
  58. return new _native.DeviceInputSystem(engine);
  59. }
  60. return new DeviceInputSystem(engine);
  61. }
  62. // Public functions
  63. /**
  64. * Checks for current device input value, given an id and input index
  65. * @param deviceName Id of connected device
  66. * @param inputIndex Index of device input
  67. * @returns Current value of input
  68. */
  69. /**
  70. * Checks for current device input value, given an id and input index. Throws exception if requested device not initialized.
  71. * @param deviceType Enum specifiying device type
  72. * @param deviceSlot "Slot" or index that device is referenced in
  73. * @param inputIndex Id of input to be checked
  74. * @returns Current value of input
  75. */
  76. public pollInput(deviceType: DeviceType, deviceSlot: number, inputIndex: number): number {
  77. const device = this._inputs[deviceType][deviceSlot];
  78. if (!device) {
  79. throw `Unable to find device ${DeviceType[deviceType]}`;
  80. }
  81. this._updateDevice(deviceType, deviceSlot, inputIndex);
  82. if (device[inputIndex] === undefined) {
  83. throw `Unable to find input ${inputIndex} for device ${DeviceType[deviceType]} in slot ${deviceSlot}`;
  84. }
  85. return device[inputIndex];
  86. }
  87. /**
  88. * Dispose of all the eventlisteners
  89. */
  90. public dispose() {
  91. // Keyboard Events
  92. if (this._keyboardActive) {
  93. window.removeEventListener("keydown", this._keyboardDownEvent);
  94. window.removeEventListener("keyup", this._keyboardUpEvent);
  95. }
  96. // Pointer Events
  97. if (this._pointerActive) {
  98. this._elementToAttachTo.removeEventListener("pointermove", this._pointerMoveEvent);
  99. this._elementToAttachTo.removeEventListener("pointerdown", this._pointerDownEvent);
  100. this._elementToAttachTo.removeEventListener("pointerup", this._pointerUpEvent);
  101. }
  102. // Gamepad Events
  103. window.removeEventListener("gamepadconnected", this._gamepadConnectedEvent);
  104. window.removeEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  105. }
  106. // Private functions
  107. /**
  108. * Add device and inputs to device array
  109. * @param deviceType Enum specifiying device type
  110. * @param deviceSlot "Slot" or index that device is referenced in
  111. * @param numberOfInputs Number of input entries to create for given device
  112. */
  113. private _registerDevice(deviceType: DeviceType, deviceSlot: number, numberOfInputs: number) {
  114. if (!this._inputs[deviceType]) {
  115. this._inputs[deviceType] = [];
  116. }
  117. if (!this._inputs[deviceType][deviceSlot]) {
  118. const device = new Array<number>(numberOfInputs);
  119. for (let i = 0; i < numberOfInputs; i++) {
  120. device[i] = 0; /* set device input as unpressed */
  121. }
  122. this._inputs[deviceType][deviceSlot] = device;
  123. this.onDeviceConnected(deviceType, deviceSlot);
  124. }
  125. }
  126. /**
  127. * Given a specific device name, remove that device from the device map
  128. * @param deviceType Enum specifiying device type
  129. * @param deviceSlot "Slot" or index that device is referenced in
  130. */
  131. private _unregisterDevice(deviceType: DeviceType, deviceSlot: number) {
  132. if (this._inputs[deviceType][deviceSlot]) {
  133. delete this._inputs[deviceType][deviceSlot];
  134. this.onDeviceDisconnected(deviceType, deviceSlot);
  135. }
  136. }
  137. /**
  138. * Handle all actions that come from keyboard interaction
  139. */
  140. private _handleKeyActions() {
  141. this._keyboardDownEvent = ((evt) => {
  142. if (!this._keyboardActive) {
  143. this._keyboardActive = true;
  144. this._registerDevice(DeviceType.Keyboard, 0, DeviceInputSystem._MAX_KEYCODES);
  145. }
  146. const kbKey = this._inputs[DeviceType.Keyboard][0];
  147. if (kbKey) {
  148. if (this.onInputChanged) {
  149. this.onInputChanged(DeviceType.Keyboard, 0, evt.keyCode, kbKey[evt.keyCode], 1);
  150. }
  151. kbKey[evt.keyCode] = 1;
  152. }
  153. });
  154. this._keyboardUpEvent = ((evt) => {
  155. const kbKey = this._inputs[DeviceType.Keyboard][0];
  156. if (kbKey) {
  157. if (this.onInputChanged) {
  158. this.onInputChanged(DeviceType.Keyboard, 0, evt.keyCode, kbKey[evt.keyCode], 0);
  159. }
  160. kbKey[evt.keyCode] = 0;
  161. }
  162. });
  163. window.addEventListener("keydown", this._keyboardDownEvent);
  164. window.addEventListener("keyup", this._keyboardUpEvent);
  165. }
  166. /**
  167. * Handle all actions that come from pointer interaction
  168. */
  169. private _handlePointerActions() {
  170. this._pointerMoveEvent = ((evt) => {
  171. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  172. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  173. if (!this._inputs[deviceType]) {
  174. this._inputs[deviceType] = [];
  175. }
  176. if (!this._inputs[deviceType][deviceSlot]) {
  177. this._pointerActive = true;
  178. this._registerDevice(deviceType, deviceSlot, DeviceInputSystem._MAX_POINTER_INPUTS);
  179. const pointer = this._inputs[deviceType][deviceSlot]; /* initalize our pointer position immediately after registration */
  180. pointer[0] = evt.clientX;
  181. pointer[1] = evt.clientY;
  182. }
  183. const pointer = this._inputs[deviceType][deviceSlot];
  184. if (pointer) {
  185. if (this.onInputChanged) {
  186. this.onInputChanged(deviceType, deviceSlot, 0, pointer[0], evt.clientX);
  187. this.onInputChanged(deviceType, deviceSlot, 1, pointer[1], evt.clientY);
  188. }
  189. pointer[0] = evt.clientX;
  190. pointer[1] = evt.clientY;
  191. }
  192. });
  193. this._pointerDownEvent = ((evt) => {
  194. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  195. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  196. if (!this._inputs[deviceType]) {
  197. this._inputs[deviceType] = [];
  198. }
  199. if (!this._inputs[deviceType][deviceSlot]) {
  200. this._pointerActive = true;
  201. this._registerDevice(deviceType, deviceSlot, DeviceInputSystem._MAX_POINTER_INPUTS);
  202. const pointer = this._inputs[deviceType][deviceSlot]; /* initalize our pointer position immediately after registration */
  203. pointer[0] = evt.clientX;
  204. pointer[1] = evt.clientY;
  205. }
  206. const pointer = this._inputs[deviceType][deviceSlot];
  207. if (pointer) {
  208. if (this.onInputChanged) {
  209. this.onInputChanged(deviceType, deviceSlot, 0, pointer[0], evt.clientX);
  210. this.onInputChanged(deviceType, deviceSlot, 1, pointer[1], evt.clientY);
  211. this.onInputChanged(deviceType, deviceSlot, evt.button + 2, pointer[evt.button + 2], 1);
  212. }
  213. pointer[0] = evt.clientX;
  214. pointer[1] = evt.clientY;
  215. pointer[evt.button + 2] = 1;
  216. }
  217. });
  218. this._pointerUpEvent = ((evt) => {
  219. const deviceType = (evt.pointerType == "mouse") ? DeviceType.Mouse : DeviceType.Touch;
  220. const deviceSlot = (evt.pointerType == "mouse") ? 0 : evt.pointerId;
  221. const pointer = this._inputs[deviceType][deviceSlot];
  222. if (pointer) {
  223. if (this.onInputChanged) {
  224. this.onInputChanged(deviceType, deviceSlot, evt.button + 2, pointer[evt.button + 2], 0);
  225. }
  226. pointer[evt.button + 2] = 0;
  227. }
  228. // We don't want to unregister the mouse because we may miss input data when a mouse is moving after a click
  229. if (evt.pointerType != "mouse") {
  230. this._unregisterDevice(deviceType, deviceSlot);
  231. }
  232. });
  233. this._elementToAttachTo.addEventListener("pointermove", this._pointerMoveEvent);
  234. this._elementToAttachTo.addEventListener("pointerdown", this._pointerDownEvent);
  235. this._elementToAttachTo.addEventListener("pointerup", this._pointerUpEvent);
  236. }
  237. /**
  238. * Handle all actions that come from gamepad interaction
  239. */
  240. private _handleGamepadActions() {
  241. this._gamepadConnectedEvent = ((evt: any) => {
  242. const deviceType = this._getGamepadDeviceType(evt.gamepad.id);
  243. const deviceSlot = evt.gamepad.index;
  244. this._registerDevice(deviceType, deviceSlot, evt.gamepad.buttons.length + evt.gamepad.axes.length);
  245. this._gamepads = this._gamepads || new Array<string>(evt.gamepad.index + 1);
  246. this._gamepads[deviceSlot] = deviceType;
  247. });
  248. this._gamepadDisconnectedEvent = ((evt: any) => {
  249. if (this._gamepads) {
  250. const deviceType = this._getGamepadDeviceType(evt.gamepad.id);
  251. const deviceSlot = evt.gamepad.index;
  252. this._unregisterDevice(deviceType, deviceSlot);
  253. delete this._gamepads[deviceSlot];
  254. }
  255. });
  256. window.addEventListener("gamepadconnected", this._gamepadConnectedEvent);
  257. window.addEventListener("gamepaddisconnected", this._gamepadDisconnectedEvent);
  258. }
  259. /**
  260. * Update all non-event based devices with each frame
  261. * @param deviceType Enum specifiying device type
  262. * @param deviceSlot "Slot" or index that device is referenced in
  263. * @param inputIndex Id of input to be checked
  264. */
  265. private _updateDevice(deviceType: DeviceType, deviceSlot: number, inputIndex: number) {
  266. // Gamepads
  267. const gp = navigator.getGamepads()[deviceSlot];
  268. if (gp && deviceType == this._gamepads[deviceSlot]) {
  269. const device = this._inputs[deviceType][deviceSlot];
  270. if (inputIndex >= gp.buttons.length) {
  271. device[inputIndex] = gp.axes[inputIndex - gp.buttons.length].valueOf();
  272. }
  273. else {
  274. device[inputIndex] = gp.buttons[inputIndex].value;
  275. }
  276. }
  277. }
  278. /**
  279. * Gets DeviceType from the device name
  280. * @param deviceName Name of Device from DeviceInputSystem
  281. * @returns DeviceType enum value
  282. */
  283. private _getGamepadDeviceType(deviceName: string): DeviceType {
  284. if (deviceName.indexOf("054c") !== -1) { // DualShock 4 Gamepad
  285. return DeviceType.DualShock;
  286. }
  287. else if (deviceName.indexOf("Xbox One") !== -1 || deviceName.search("Xbox 360") !== -1 || deviceName.search("xinput") !== -1) { // Xbox Gamepad
  288. return DeviceType.Xbox;
  289. }
  290. else if (deviceName.indexOf("057e") !== -1) { // Switch Gamepad
  291. return DeviceType.Switch;
  292. }
  293. return DeviceType.Generic;
  294. }
  295. }