Files
GDevelop/GDJS/Runtime/inputmanager.ts

643 lines
19 KiB
TypeScript

/*
* 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.
*/
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<boolean>;
private _justPressedKeys: Hashtable<boolean>;
private _releasedKeys: Hashtable<boolean>;
private _lastPressedKey: float = 0;
private _pressedMouseButtons: Array<boolean>;
private _releasedMouseButtons: Array<boolean>;
/**
* 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;
// 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<Touch>;
//Identifiers of the touches that started during/before the frame.
private _startedTouches: Array<integer> = [];
//Identifiers of the touches that ended during/before the frame.
private _endedTouches: Array<integer> = [];
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;
}
/**
* Should be called when the mouse is moved.
*
* 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
*/
onMouseMove(x: float, y: float): void {
this._setCursorPosition(x, y);
this._mouseX = x;
this._mouseY = y;
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;
}
/**
* 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<integer> {
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._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<integer> = [];
}
}