Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions GDJS/Runtime/inputmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ namespace gdjs {
* if location is not specified.
*/
private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91];

/**
* The `KeyboardEvent.location` of keys on the numeric keypad.
*/
private static _NUMPAD_LOCATION: integer = 3;
private _pressedKeys: Hashtable<boolean>;
private _justPressedKeys: Hashtable<boolean>;
private _releasedKeys: Hashtable<boolean>;
Expand Down Expand Up @@ -111,11 +116,24 @@ namespace gdjs {
*
* @param keyCode The raw key code
* @param location The location
* @param code The KeyboardEvent.code, used to know if the key really is on
* the numpad when a numpad location is reported.
*/
static getLocationAwareKeyCode(
keyCode: number,
location: number | null | undefined
location: number | null | undefined,
code?: string | null
): integer {
if (
location === InputManager._NUMPAD_LOCATION &&
code &&
!code.startsWith('Numpad')
) {
// macOS sets the "numeric pad" modifier flag for the arrow keys, which makes
// WebKit (Safari) report them with a numpad location. Only `code` tells the
// numpad keys apart from the arrow keys, so trust it over the location.
location = 0;
}
if (location) {
// If it is a numpad number, do not modify it.
if (96 <= keyCode && keyCode <= 105) {
Expand All @@ -135,11 +153,13 @@ namespace gdjs {
* 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.
* @param code The KeyboardEvent.code of the event.
*/
onKeyPressed(keyCode: number, location?: number): void {
onKeyPressed(keyCode: number, location?: number, code?: string): void {
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
keyCode,
location
location,
code
);
this._pressedKeys.put(locationAwareKeyCode, true);
this._justPressedKeys.put(locationAwareKeyCode, true);
Expand All @@ -152,11 +172,13 @@ namespace gdjs {
* 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.
* @param code The KeyboardEvent.code of the event.
*/
onKeyReleased(keyCode: number, location?: number): void {
onKeyReleased(keyCode: number, location?: number, code?: string): void {
const locationAwareKeyCode = InputManager.getLocationAwareKeyCode(
keyCode,
location
location,
code
);
this._pressedKeys.put(locationAwareKeyCode, false);
this._justPressedKeys.put(locationAwareKeyCode, false);
Expand All @@ -170,6 +192,11 @@ namespace gdjs {
*/
releaseAllPressedKeys(): void {
for (const locationAwareKeyCode in this._pressedKeys.items) {
// Keys keep an entry once released, so only the keys actually held down
// must be made to go through the release state.
if (!this._pressedKeys.items[locationAwareKeyCode]) {
continue;
}
this._pressedKeys.put(locationAwareKeyCode, false);
this._justPressedKeys.put(locationAwareKeyCode, false);
this._releasedKeys.put(locationAwareKeyCode, true);
Expand Down Expand Up @@ -427,6 +454,23 @@ namespace gdjs {
this._releasedMouseButtons[buttonCode] = true;
}

/**
* Release all the mouse buttons that are currently pressed.
*/
releaseAllPressedMouseButtons(): void {
for (
let buttonCode = 0;
buttonCode < this._pressedMouseButtons.length;
buttonCode++
) {
if (this._pressedMouseButtons[buttonCode]) {
// Go through `onMouseButtonReleased` so that the touch simulated by
// the left button is ended too.
this.onMouseButtonReleased(buttonCode);
}
}
}

/**
* Return true if the mouse button corresponding to buttonCode is pressed.
* @param buttonCode The mouse button code (0: Left button, 1: Right button).
Expand Down
48 changes: 40 additions & 8 deletions GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ namespace gdjs {

_wasDisposed: boolean = false;

_unregisterFocusListeners: (() => void) | null = null;

/**
* @param game The game that is being rendered
* @param forceFullscreen If fullscreen should be always activated
Expand Down Expand Up @@ -753,7 +755,7 @@ namespace gdjs {
return;
}

manager.onKeyPressed(e.keyCode, e.location);
manager.onKeyPressed(e.keyCode, e.location, e.code);
};
document.onkeyup = (e) => {
if (isFocusingDomElement()) {
Expand All @@ -770,11 +772,11 @@ namespace gdjs {
// This means the key would be considered as "stuck" from the game's perspective
// it would never be released unless it's pressed and released again (without meta).
// Out of caution, we simulate a release of the key that were pressed with meta key.
for (const {
location,
keyCode,
} of keysPressedWithMetaPressedByCode.values()) {
manager.onKeyReleased(keyCode, location);
for (const [
code,
{ location, keyCode },
] of keysPressedWithMetaPressedByCode) {
manager.onKeyReleased(keyCode, location, code);
}
keysPressedWithMetaPressedByCode.clear();
}
Expand All @@ -792,7 +794,32 @@ namespace gdjs {
e.preventDefault();
}

manager.onKeyReleased(e.keyCode, e.location);
manager.onKeyReleased(e.keyCode, e.location, e.code);
};

// No "keyup" or "mouseup" is received for the keys and mouse buttons that are
// still held down when the game loses the focus (when switching to another
// window or tab), which would leave them stuck in a pressed state.
// Release them all instead.
const releaseAllPressedInputs = () => {
keysPressedWithMetaPressedByCode.clear();
manager.releaseAllPressedKeys();
manager.releaseAllPressedMouseButtons();
};
const onBlur = () => {
releaseAllPressedInputs();
};
const onVisibilityChange = () => {
// On mobile, switching to another app can hide the game without blurring it.
if (document.visibilityState === 'hidden') {
releaseAllPressedInputs();
}
};
window.addEventListener('blur', onBlur);
document.addEventListener('visibilitychange', onVisibilityChange);
this._unregisterFocusListeners = () => {
window.removeEventListener('blur', onBlur);
document.removeEventListener('visibilitychange', onVisibilityChange);
};

// Mouse:
Expand Down Expand Up @@ -1099,11 +1126,16 @@ namespace gdjs {
/**
* Dispose the renderers (PixiJS and/or Three.js) as well as DOM elements
* used for the game (the canvas, if specified, and the additional DOM container
* created on top of it to allow display HTML elements, for example for text inputs).
* created on top of it to allow display HTML elements, for example for text inputs),
* and the events listeners registered on the window and document.
*
* @param removeCanvas If true, the canvas will be removed from the DOM.
*/
dispose(removeCanvas?: boolean) {
if (this._unregisterFocusListeners) {
this._unregisterFocusListeners();
this._unregisterFocusListeners = null;
}
this._pixiRenderer?.destroy();
this._threeRenderer?.dispose();
this._pixiRenderer = null;
Expand Down
68 changes: 68 additions & 0 deletions GDJS/tests/tests/inputmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,74 @@ describe('gdjs.InputManager', () => {
inputManager.onKeyReleased(17);
});

it('should ignore a numpad location that is contradicted by the code', () => {
// Safari on macOS reports the arrow keys with a numpad location, because macOS
// sets the "numeric pad" modifier flag for them.
inputManager.onKeyPressed(38, 3, 'ArrowUp');
expect(inputManager.getLastPressedKey()).to.be(38);
expect(inputManager.isKeyPressed(38)).to.be(true);
expect(inputManager.isKeyPressed(3038)).to.be(false);
inputManager.onKeyReleased(38, 3, 'ArrowUp');
expect(inputManager.wasKeyReleased(38)).to.be(true);
expect(inputManager.wasKeyReleased(3038)).to.be(false);
inputManager.onFrameEnded();

// A numpad key with NumLock off keeps its numpad location.
inputManager.onKeyPressed(38, 3, 'Numpad8');
expect(inputManager.getLastPressedKey()).to.be(3038);
expect(inputManager.isKeyPressed(3038)).to.be(true);
expect(inputManager.isKeyPressed(38)).to.be(false);
inputManager.onKeyReleased(38, 3, 'Numpad8');
expect(inputManager.wasKeyReleased(3038)).to.be(true);
inputManager.onFrameEnded();

// Without a code, the location is trusted as before.
inputManager.onKeyPressed(38, 3);
expect(inputManager.getLastPressedKey()).to.be(3038);
inputManager.onKeyReleased(38, 3);
});

it('should release the held keys and mouse buttons when the game loses the focus', () => {
// A key that was already pressed and released before losing the focus.
inputManager.onKeyPressed(65, 0, 'KeyA');
inputManager.onKeyReleased(65, 0, 'KeyA');
inputManager.onFrameEnded();

// Keys and mouse buttons still held down when the focus is lost.
inputManager.onKeyPressed(87, 0, 'KeyW');
inputManager.onKeyPressed(16, 1, 'ShiftLeft');
inputManager.onMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON);
expect(inputManager.isKeyPressed(87)).to.be(true);
expect(inputManager.isKeyPressed(1016)).to.be(true);
expect(
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
).to.be(true);
inputManager.onFrameEnded();

inputManager.releaseAllPressedKeys();
inputManager.releaseAllPressedMouseButtons();
expect(inputManager.isKeyPressed(87)).to.be(false);
expect(inputManager.isKeyPressed(1016)).to.be(false);
expect(inputManager.wasKeyReleased(87)).to.be(true);
expect(inputManager.wasKeyReleased(1016)).to.be(true);
expect(inputManager.anyKeyPressed()).to.be(false);
expect(
inputManager.isMouseButtonPressed(gdjs.InputManager.MOUSE_LEFT_BUTTON)
).to.be(false);
expect(
inputManager.isMouseButtonReleased(gdjs.InputManager.MOUSE_LEFT_BUTTON)
).to.be(true);
expect(inputManager.anyMouseButtonPressed()).to.be(false);
// The touch simulated by the left button must have ended too.
expect(
inputManager.hasTouchEnded(gdjs.InputManager.MOUSE_TOUCH_ID)
).to.be(true);

// The key released before the focus loss must not be released a second time.
expect(inputManager.wasKeyReleased(65)).to.be(false);
inputManager.onFrameEnded();
});

it('should handle mouse events', () => {
inputManager.onMouseMove(500, 600);
expect(inputManager.getCursorX()).to.be(500);
Expand Down
Loading