/* * GDevelop JS Platform * Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved. * This project is released under the MIT License. */ namespace gdjs { type Touch = { x: float; y: float; justEnded: boolean }; /** * Store input made on a canvas: mouse position, key pressed * and touches states. * @category Core Engine > Input */ export class InputManager { static MOUSE_LEFT_BUTTON: integer = 0; static MOUSE_RIGHT_BUTTON: integer = 1; static MOUSE_MIDDLE_BUTTON: integer = 2; static MOUSE_BACK_BUTTON: integer = 3; static MOUSE_FORWARD_BUTTON: integer = 4; static MOUSE_TOUCH_ID: integer = 1; /** * Holds the raw keyCodes of the keys which only have left/right * variants and should default to their left variant values * if location is not specified. */ private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91]; private _pressedKeys: Hashtable; private _justPressedKeys: Hashtable; private _releasedKeys: Hashtable; private _lastPressedKey: float = 0; private _pressedMouseButtons: Array; private _releasedMouseButtons: Array; /** * The cursor X position (moved by mouse and touch events). */ private _cursorX: float = 0; /** * The cursor Y position (moved by mouse and touch events). */ private _cursorY: float = 0; /** * The mouse X position (only moved by mouse events). */ private _mouseX: float = 0; /** * The mouse Y position (only moved by mouse events). */ private _mouseY: float = 0; private _isMouseInsideCanvas: boolean = true; private _wheelDeltaX: float = 0; private _wheelDeltaY: float = 0; private _wheelDeltaZ: float = 0; /** * The mouse movement X (only moved by mouse events). */ private _mouseMovementX: float = 0; /** * The mouse movement Y (only moved by mouse events). */ private _mouseMovementY: float = 0; // TODO Remove _touches when there is no longer SpritePanelButton 1.2.0 // extension in the wild. // @ts-ignore private _touches = { firstKey: (): string | number | null => { for (const key in this._mouseOrTouches.items) { // Exclude mouse key. if (key !== '1') { return key; } } return null; }, }; private _mouseOrTouches: Hashtable; //Identifiers of the touches that started during/before the frame. private _startedTouches: Array = []; //Identifiers of the touches that ended during/before the frame. private _endedTouches: Array = []; private _touchSimulateMouse: boolean = true; /** * @deprecated */ private _lastStartedTouchIndex = 0; /** * @deprecated */ private _lastEndedTouchIndex = 0; constructor() { this._pressedKeys = new Hashtable(); this._justPressedKeys = new Hashtable(); this._releasedKeys = new Hashtable(); this._pressedMouseButtons = new Array(5); this._releasedMouseButtons = new Array(5); this._mouseOrTouches = new Hashtable(); } /** * Returns the "location-aware" keyCode, given a raw keyCode * and location. The location corresponds to KeyboardEvent.location, * which should be 0 for standard keys, 1 for left keys, * 2 for right keys, and 3 for numpad keys. * * @param keyCode The raw key code * @param location The location */ static getLocationAwareKeyCode( keyCode: number, location: number | null | undefined ): integer { if (location) { // If it is a numpad number, do not modify it. if (96 <= keyCode && keyCode <= 105) { return keyCode; } return keyCode + 1000 * location; } if (InputManager._DEFAULT_LEFT_VARIANT_KEYS.indexOf(keyCode) !== -1) { return keyCode + 1000; } return keyCode; } /** * Should be called whenever a key is pressed. The location corresponds to * KeyboardEvent.location, which should be 0 for standard keys, 1 for left keys, * 2 for right keys, and 3 for numpad keys. * @param keyCode The raw key code associated to the key press. * @param location The location of the event. */ onKeyPressed(keyCode: number, location?: number): void { const locationAwareKeyCode = InputManager.getLocationAwareKeyCode( keyCode, location ); this._pressedKeys.put(locationAwareKeyCode, true); this._justPressedKeys.put(locationAwareKeyCode, true); this._lastPressedKey = locationAwareKeyCode; } /** * Should be called whenever a key is released. The location corresponds to * KeyboardEvent.location, which should be 0 for standard keys, 1 for left keys, * 2 for right keys, and 3 for numpad keys. * @param keyCode The raw key code associated to the key release. * @param location The location of the event. */ onKeyReleased(keyCode: number, location?: number): void { const locationAwareKeyCode = InputManager.getLocationAwareKeyCode( keyCode, location ); this._pressedKeys.put(locationAwareKeyCode, false); this._justPressedKeys.put(locationAwareKeyCode, false); this._releasedKeys.put(locationAwareKeyCode, true); } /** * Release all keys that are currently pressed. * Note: if you want to discard pressed keys without considering them as * released, check `clearAllPressedKeys` instead. */ releaseAllPressedKeys(): void { for (const locationAwareKeyCode in this._pressedKeys.items) { this._pressedKeys.put(locationAwareKeyCode, false); this._justPressedKeys.put(locationAwareKeyCode, false); this._releasedKeys.put(locationAwareKeyCode, true); } } /** * Clears all stored pressed keys without making the keys go through * the release state. * Note: prefer to use `releaseAllPressedKeys` instead, as it corresponds * to a normal key release. */ clearAllPressedKeys(): void { this._pressedKeys.clear(); this._justPressedKeys.clear(); } /** * Return the location-aware code of the last key that was pressed. * @return The location-aware code of the last key pressed. */ getLastPressedKey(): number { return this._lastPressedKey; } /** * Return true if the key corresponding to the location-aware keyCode is pressed * (either it was just pressed or is still held down). * @param locationAwareKeyCode The location-aware key code to be tested. */ isKeyPressed(locationAwareKeyCode: number): boolean { return !!this._pressedKeys.get(locationAwareKeyCode); } /** * Return true if the key corresponding to the location-aware keyCode * was just pressed during the last frame. * @param locationAwareKeyCode The location-aware key code to be tested. */ wasKeyJustPressed(locationAwareKeyCode: number): boolean { return !!this._justPressedKeys.get(locationAwareKeyCode); } /** * Return true if the key corresponding to the location-aware keyCode was released during the last frame. * @param locationAwareKeyCode The location-aware key code to be tested. */ wasKeyReleased(locationAwareKeyCode: number) { return !!this._releasedKeys.get(locationAwareKeyCode); } /** * Return true if any key is pressed. * @return true if any key is pressed. */ anyKeyPressed(): boolean { for (const keyCode in this._pressedKeys.items) { if (this._pressedKeys.items.hasOwnProperty(keyCode)) { if (this._pressedKeys.items[keyCode]) { return true; } } } return false; } /** * Return true if any key is released. * @return true if any key is released. */ anyKeyReleased(): boolean { for (const keyCode in this._releasedKeys.items) { if (this._releasedKeys.items.hasOwnProperty(keyCode)) { if (this._releasedKeys.items[keyCode]) { return true; } } } return false; } exceptionallyGetAllJustPressedKeys(): number[] { const result: string[] = []; this._justPressedKeys.keys(result); return result.map((locationAwareKeyCode) => parseInt(locationAwareKeyCode, 10) ); } /** * Should be called when the mouse is moved. * Some browsers or environments may call this function multiple times during a single frame. * * Please note that the coordinates must be expressed relative to the view position. * * @param x The mouse new X position * @param y The mouse new Y position * @param options An object containing the mouse movement X and Y if available. */ onMouseMove( x: float, y: float, options?: { movementX: float; movementY: float } ): void { this._setCursorPosition(x, y); this._mouseX = x; this._mouseY = y; if (options) { // Mouse movement can be accumulated over multiple calls to onMouseMove during a single frame. // This is the case with Firefox which calls onMouseMove multiple times, including with // values being 0 (so we can't just rely on the last one). const { movementX, movementY } = options; if (movementX !== undefined) this._mouseMovementX += movementX; if (movementY !== undefined) this._mouseMovementY += movementY; } if (this.isMouseButtonPressed(InputManager.MOUSE_LEFT_BUTTON)) { this._moveTouch( InputManager.MOUSE_TOUCH_ID, this.getCursorX(), this.getCursorY() ); } } _setCursorPosition(x: float, y: float): void { this._cursorX = x; this._cursorY = y; } /** * Get the cursor X position. * The cursor is moved by mouse and touch events. * * @return the cursor X position, relative to the game view. */ getCursorX(): float { return this._cursorX; } /** * Get the cursor Y position. * The cursor is moved by mouse and touch events. * * @return the cursor Y position, relative to the game view. */ getCursorY(): float { return this._cursorY; } /** * Get the mouse X position. * * @return the mouse X position, relative to the game view. */ getMouseX(): float { return this._mouseX; } /** * Get the mouse Y position. * * @return the mouse Y position, relative to the game view. */ getMouseY(): float { return this._mouseY; } /** * Get the mouse movement on X axis. * * @return the mouse movement X. */ getMouseMovementX(): float { return this._mouseMovementX; } /** * Get the mouse movement on Y axis. * * @return the mouse movement Y. */ getMouseMovementY(): float { return this._mouseMovementY; } /** * Should be called when the mouse leave the canvas. */ onMouseLeave(): void { this._isMouseInsideCanvas = false; } /** * Should be called when the mouse enter the canvas. */ onMouseEnter(): void { this._isMouseInsideCanvas = true; } /** * @return true when the mouse is inside the canvas. */ isMouseInsideCanvas(): boolean { return this._isMouseInsideCanvas; } /** * Should be called whenever a mouse button is pressed. * @param buttonCode The mouse button code associated to the event. * See InputManager.MOUSE_LEFT_BUTTON, InputManager.MOUSE_RIGHT_BUTTON, InputManager.MOUSE_MIDDLE_BUTTON */ onMouseButtonPressed(buttonCode: number): void { this._setMouseButtonPressed(buttonCode); if (buttonCode === InputManager.MOUSE_LEFT_BUTTON) { this._addTouch( InputManager.MOUSE_TOUCH_ID, this.getCursorX(), this.getCursorY() ); } } /** * Return true if any mouse button is pressed. * @return true if any mouse button is pressed. */ anyMouseButtonPressed(): boolean { for (const buttonCode in this._pressedMouseButtons) { if (this._pressedMouseButtons[buttonCode]) { return true; } } return false; } _setMouseButtonPressed(buttonCode: number): void { this._pressedMouseButtons[buttonCode] = true; this._releasedMouseButtons[buttonCode] = false; } /** * Should be called whenever a mouse button is released. * @param buttonCode The mouse button code associated to the event. (see onMouseButtonPressed) */ onMouseButtonReleased(buttonCode: number): void { this._setMouseButtonReleased(buttonCode); if (buttonCode === InputManager.MOUSE_LEFT_BUTTON) { this._removeTouch(InputManager.MOUSE_TOUCH_ID); } } _setMouseButtonReleased(buttonCode: number): void { this._pressedMouseButtons[buttonCode] = false; this._releasedMouseButtons[buttonCode] = true; } /** * Return true if the mouse button corresponding to buttonCode is pressed. * @param buttonCode The mouse button code (0: Left button, 1: Right button). */ isMouseButtonPressed(buttonCode: number): boolean { return ( this._pressedMouseButtons[buttonCode] !== undefined && this._pressedMouseButtons[buttonCode] ); } /** * Return true if the mouse button corresponding to buttonCode was just released. * @param buttonCode The mouse button code (0: Left button, 1: Right button). */ isMouseButtonReleased(buttonCode: number): boolean { return ( this._releasedMouseButtons[buttonCode] !== undefined && this._releasedMouseButtons[buttonCode] ); } /** * Should be called whenever the mouse wheel is used * @param wheelDeltaY The mouse wheel delta */ onMouseWheel( wheelDeltaY: number, wheelDeltaX: number, wheelDeltaZ: number ): void { this._wheelDeltaY = wheelDeltaY; if (wheelDeltaX !== undefined) this._wheelDeltaX = wheelDeltaX; if (wheelDeltaZ !== undefined) this._wheelDeltaZ = wheelDeltaZ; } /** * Return the mouse wheel delta on Y axis. */ getMouseWheelDelta(): float { return this._wheelDeltaY; } /** * Return the mouse wheel delta on X axis. */ getMouseWheelDeltaX(): float { return this._wheelDeltaX; } /** * Return the mouse wheel delta on Z axis. */ getMouseWheelDeltaZ(): float { return this._wheelDeltaZ; } /** * Get a touch X position. * * @return the touch X position, relative to the game view. */ getTouchX(publicIdentifier: integer): float { if (!this._mouseOrTouches.containsKey(publicIdentifier)) { return 0; } return this._mouseOrTouches.get(publicIdentifier).x; } /** * Get a touch Y position. * * @return the touch Y position, relative to the game view. */ getTouchY(publicIdentifier: integer): float { if (!this._mouseOrTouches.containsKey(publicIdentifier)) { return 0; } return this._mouseOrTouches.get(publicIdentifier).y; } /** * @param publicIdentifier the touch identifier * @returns true if the touch has just ended. */ hasTouchEnded(publicIdentifier: integer): boolean { // A touch that end then start in one frame is ignored // because it's probably noise. // See _addTouch if (!this._mouseOrTouches.containsKey(publicIdentifier)) { return false; } return this._mouseOrTouches.get(publicIdentifier).justEnded; } /** * Update and return the array containing the identifiers of all touches. */ getAllTouchIdentifiers(): Array { InputManager._allTouchIds.length = 0; for (const id in this._mouseOrTouches.items) { if (this._mouseOrTouches.items.hasOwnProperty(id)) { InputManager._allTouchIds.push(parseInt(id, 10)); } } return InputManager._allTouchIds; } onTouchStart(rawIdentifier: integer, x: float, y: float): void { this._addTouch(this.getPublicTouchIdentifier(rawIdentifier), x, y); if (this._touchSimulateMouse) { this._setCursorPosition(x, y); this._setMouseButtonPressed(InputManager.MOUSE_LEFT_BUTTON); } } _addTouch(publicIdentifier: integer, x: float, y: float): void { // A touch that end then start in one frame is ignored // because it's probably noise. if (!this._endedTouches.includes(publicIdentifier)) { this._startedTouches.push(publicIdentifier); this._mouseOrTouches.put(publicIdentifier, { x: x, y: y, justEnded: false, }); } } onTouchMove(rawIdentifier: integer, x: float, y: float): void { this._moveTouch(this.getPublicTouchIdentifier(rawIdentifier), x, y); if (this._touchSimulateMouse) { this._setCursorPosition(x, y); } } _moveTouch(publicIdentifier: integer, x: float, y: float): void { const touch = this._mouseOrTouches.get(publicIdentifier); if (!touch) { return; } touch.x = x; touch.y = y; } onTouchEnd(rawIdentifier: number): void { this._removeTouch(this.getPublicTouchIdentifier(rawIdentifier)); if (this._touchSimulateMouse) { this._setMouseButtonReleased(InputManager.MOUSE_LEFT_BUTTON); } } onTouchCancel(rawIdentifier: number): void { // Don't do anything specific for canceled touches to avoid to make // touches handling more complex for users. this.onTouchEnd(rawIdentifier); } _removeTouch(publicIdentifier: number): void { this._endedTouches.push(publicIdentifier); if (this._mouseOrTouches.containsKey(publicIdentifier)) { //Postpone deletion at the end of the frame this._mouseOrTouches.get(publicIdentifier).justEnded = true; } } /** * Add 2 to the identifier to avoid identifiers taking the GDevelop default * variable value which is 0 and reserve 1 for the mouse. * @param rawIdentifier The identifier given by the browser. * @returns The identifier used in events. */ private getPublicTouchIdentifier(rawIdentifier: integer): integer { return rawIdentifier + 2; } getStartedTouchIdentifiers(): integer[] { return this._startedTouches; } /** * @deprecated */ popStartedTouch(): integer | undefined { const publicIdentifier = this._startedTouches[this._lastStartedTouchIndex]; this._lastStartedTouchIndex++; return publicIdentifier; } /** * @deprecated */ popEndedTouch(): integer | undefined { const publicIdentifier = this._endedTouches[this._lastEndedTouchIndex]; this._lastEndedTouchIndex++; return publicIdentifier; } /** * Set if touch events should simulate mouse events. * * If true, any touch will move the mouse position and set mouse buttons * as pressed/released. * @param enable true to simulate mouse events, false to disable it. */ touchSimulateMouse(enable: boolean): void { if (enable === undefined) { enable = true; } this._touchSimulateMouse = enable; } /** * @returns true if the touch events are used to simulate mouse events. */ isSimulatingMouseWithTouch(): boolean { return this._touchSimulateMouse; } /** * Notify the input manager that the frame ended, so anything that last * only for one frame (started/ended touches) should be reset. * * This method should be called in the game loop (see `gdjs.RuntimeGame.startGameLoop`). * You don't need to call it otherwise. */ onFrameEnded(): void { //Only clear the ended touches at the end of the frame. for (const id in this._mouseOrTouches.items) { if (this._mouseOrTouches.items.hasOwnProperty(id)) { const touch = this._mouseOrTouches.items[id]; if (touch.justEnded) { this._mouseOrTouches.remove(id); } } } this._startedTouches.length = 0; this._endedTouches.length = 0; this._releasedKeys.clear(); this._justPressedKeys.clear(); this._releasedMouseButtons.length = 0; this._wheelDeltaX = 0; this._wheelDeltaY = 0; this._wheelDeltaZ = 0; this._mouseMovementX = 0; this._mouseMovementY = 0; this._lastStartedTouchIndex = 0; this._lastEndedTouchIndex = 0; } /** * Return true if the mouse wheel scroll to up */ isScrollingUp(): boolean { return this.getMouseWheelDelta() > 0; } /** * Return true if the mouse wheel scroll to down */ isScrollingDown(): boolean { return this.getMouseWheelDelta() < 0; } static _allTouchIds: Array = []; } }