Skip to content
Draft
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
46 changes: 15 additions & 31 deletions GDJS/Runtime/InGameEditor/InGameEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4405,8 +4405,6 @@ namespace gdjs {
distance: float = 800;
private _isEnabled: boolean = true;

private _lastCursorX: float = 0;
private _lastCursorY: float = 0;
private _wasMouseRightButtonPressed = false;
private _wasMouseMiddleButtonPressed = false;

Expand All @@ -4430,7 +4428,6 @@ namespace gdjs {
step(): void {
const runtimeGame = this._editorCamera.editor.getRuntimeGame();
const inputManager = runtimeGame.getInputManager();
const renderer = runtimeGame.getRenderer();
const isRightButtonPressed = inputManager.isMouseButtonPressed(1);
const isMiddleButtonPressed = inputManager.isMouseButtonPressed(2);

Expand All @@ -4445,13 +4442,11 @@ namespace gdjs {
// The camera should not move the 1st frame
this._wasMouseMiddleButtonPressed)
) {
// Use movement deltas when pointer is locked, otherwise use cursor position delta
const xDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementX()
: inputManager.getCursorX() - this._lastCursorX;
const yDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementY()
: inputManager.getCursorY() - this._lastCursorY;
// Movement is lock-aware: it uses the browser movement deltas when
// the pointer is locked, and the cursor position delta otherwise.
// See gdjs.InputManager.onMouseMove.
const xDelta = inputManager.getMouseMovementX();
const yDelta = inputManager.getMouseMovementY();

const rotationSpeed = 0.2;
this.rotationAngle += xDelta * rotationSpeed;
Expand Down Expand Up @@ -4524,8 +4519,6 @@ namespace gdjs {

this._wasMouseRightButtonPressed = isRightButtonPressed;
this._wasMouseMiddleButtonPressed = isMiddleButtonPressed;
this._lastCursorX = inputManager.getCursorX();
this._lastCursorY = inputManager.getCursorY();
}

getAnchorX(): float {
Expand Down Expand Up @@ -4660,8 +4653,6 @@ namespace gdjs {
private _euler: THREE.Euler = new THREE.Euler(0, 0, 0, 'ZYX');
private _rotationMatrix: THREE.Matrix4 = new THREE.Matrix4();

private _lastCursorX: float = 0;
private _lastCursorY: float = 0;
private _wasMouseRightButtonPressed = false;

// Touch gesture state
Expand All @@ -4686,7 +4677,6 @@ namespace gdjs {
step(): void {
const runtimeGame = this._editorCamera.editor.getRuntimeGame();
const inputManager = runtimeGame.getInputManager();
const renderer = runtimeGame.getRenderer();
const isRightButtonPressed = inputManager.isMouseButtonPressed(1);
if (this._isEnabled) {
const { right, up, forward } = this.getCameraVectors();
Expand Down Expand Up @@ -4813,13 +4803,11 @@ namespace gdjs {
inputManager.isMouseButtonPressed(0)) ||
(isShiftPressed(inputManager) && inputManager.isMouseButtonPressed(2))
) {
// Use movement deltas when pointer is locked, otherwise use cursor position delta
const xDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementX()
: inputManager.getCursorX() - this._lastCursorX;
const yDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementY()
: inputManager.getCursorY() - this._lastCursorY;
// Movement is lock-aware: it uses the browser movement deltas when
// the pointer is locked, and the cursor position delta otherwise.
// See gdjs.InputManager.onMouseMove.
const xDelta = inputManager.getMouseMovementX();
const yDelta = inputManager.getMouseMovementY();
moveCameraByVector(up, yDelta);
moveCameraByVector(right, -xDelta);
}
Expand All @@ -4830,13 +4818,11 @@ namespace gdjs {
// The camera should not move the 1st frame
this._wasMouseRightButtonPressed
) {
// Use movement deltas when pointer is locked, otherwise use cursor position delta
const xDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementX()
: inputManager.getCursorX() - this._lastCursorX;
const yDelta = renderer.isPointerLocked()
? inputManager.getMouseMovementY()
: inputManager.getCursorY() - this._lastCursorY;
// Movement is lock-aware: it uses the browser movement deltas when
// the pointer is locked, and the cursor position delta otherwise.
// See gdjs.InputManager.onMouseMove.
const xDelta = inputManager.getMouseMovementX();
const yDelta = inputManager.getMouseMovementY();

const rotationSpeed = 0.2;
this.rotationAngle += xDelta * rotationSpeed;
Expand All @@ -4848,8 +4834,6 @@ namespace gdjs {
this._gestureActiveTouchIds = [];
}
this._wasMouseRightButtonPressed = isRightButtonPressed;
this._lastCursorX = inputManager.getCursorX();
this._lastCursorY = inputManager.getCursorY();
}

moveForward(distanceDelta: number) {
Expand Down
38 changes: 35 additions & 3 deletions GDJS/Runtime/inputmanager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ namespace gdjs {
* The mouse movement Y (only moved by mouse events).
*/
private _mouseMovementY: float = 0;
/**
* Whether the last known mouse position can be used to compute a movement
* delta. Reset when the mouse leaves/enters the canvas or when the pointer
* lock state changes, to avoid reporting a spurious large movement.
*/
private _canComputeMouseMovementFromPosition: boolean = false;

// TODO Remove _touches when there is no longer SpritePanelButton 1.2.0
// extension in the wild.
Expand Down Expand Up @@ -273,17 +279,37 @@ namespace gdjs {
y: float,
options?: { movementX: float; movementY: float }
): void {
const previousMouseX = this._mouseX;
const previousMouseY = this._mouseY;

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).
// The pointer is locked: the browser freezes the cursor position, so the
// only reliable source of movement is the browser-provided movementX/Y.
// These can be reported across multiple onMouseMove calls during a single
// frame (notably on Firefox, including with values being 0, so we can't
// just rely on the last one), hence the accumulation.
const { movementX, movementY } = options;
if (movementX !== undefined) this._mouseMovementX += movementX;
if (movementY !== undefined) this._mouseMovementY += movementY;
// A position delta is meaningless while the pointer is locked.
this._canComputeMouseMovementFromPosition = false;
} else {
// The pointer is not locked: derive the movement from the change of the
// cursor position instead of relying on the browser movementX/Y. The
// latter are unreliable across browsers (for example Firefox adds the
// game canvas offset to them, which makes first-person cameras drift or
// stick unless the game is fullscreen) and are not expressed in the game
// resolution units. Using the position delta keeps the movement
// consistent with getCursorX/Y on every browser.
if (this._canComputeMouseMovementFromPosition) {
this._mouseMovementX += x - previousMouseX;
this._mouseMovementY += y - previousMouseY;
}
this._canComputeMouseMovementFromPosition = true;
}

if (this.isMouseButtonPressed(InputManager.MOUSE_LEFT_BUTTON)) {
Expand Down Expand Up @@ -361,13 +387,19 @@ namespace gdjs {
*/
onMouseLeave(): void {
this._isMouseInsideCanvas = false;
// The next position can be anywhere on the canvas, so don't use it to
// compute a movement delta.
this._canComputeMouseMovementFromPosition = false;
}

/**
* Should be called when the mouse enter the canvas.
*/
onMouseEnter(): void {
this._isMouseInsideCanvas = true;
// The next position can be anywhere on the canvas, so don't use it to
// compute a movement delta.
this._canComputeMouseMovementFromPosition = false;
}

/**
Expand Down
17 changes: 13 additions & 4 deletions GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,10 +811,19 @@ namespace gdjs {
}
canvas.onmousemove = (e) => {
const pos = this.convertPageToGameCoords(e.pageX, e.pageY);
manager.onMouseMove(pos[0], pos[1], {
movementX: e.movementX,
movementY: e.movementY,
});
// The browser movementX/Y are only reliable while the pointer is locked
// (the cursor position is then frozen by the browser). Otherwise they are
// inconsistent across browsers (e.g. Firefox adds the game canvas offset
// to them, breaking first-person cameras unless fullscreen), so let the
// InputManager derive the movement from the cursor position instead.
// See https://github.com/4ian/GDevelop/issues/6338.
manager.onMouseMove(
pos[0],
pos[1],
this.isPointerLocked()
? { movementX: e.movementX, movementY: e.movementY }
: undefined
);
};
canvas.onmousedown = (e) => {
const pos = this.convertPageToGameCoords(e.pageX, e.pageY);
Expand Down
65 changes: 65 additions & 0 deletions GDJS/tests/tests/inputmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,71 @@ describe('gdjs.InputManager', () => {
expect(inputManager.isMouseInsideCanvas()).to.be(true);
});

it('should compute mouse movement from the cursor position when the pointer is not locked', () => {
// Reset the movement tracking deterministically.
inputManager.onMouseEnter();

// The first move only establishes the reference position: no movement yet.
inputManager.onMouseMove(500, 600);
expect(inputManager.getMouseMovementX()).to.be(0);
expect(inputManager.getMouseMovementY()).to.be(0);

// Subsequent moves report the delta of the cursor position...
inputManager.onMouseMove(520, 615);
expect(inputManager.getMouseMovementX()).to.be(20);
expect(inputManager.getMouseMovementY()).to.be(15);

// ...accumulated over the frame.
inputManager.onMouseMove(515, 605);
expect(inputManager.getMouseMovementX()).to.be(15);
expect(inputManager.getMouseMovementY()).to.be(5);

// The movement is reset at the end of the frame, but the reference position
// is kept.
inputManager.onFrameEnded();
expect(inputManager.getMouseMovementX()).to.be(0);
expect(inputManager.getMouseMovementY()).to.be(0);

inputManager.onMouseMove(525, 600);
expect(inputManager.getMouseMovementX()).to.be(10);
expect(inputManager.getMouseMovementY()).to.be(-5);
});

it('should use the browser movement deltas when the pointer is locked', () => {
inputManager.onMouseEnter();
inputManager.onMouseMove(500, 600);
inputManager.onFrameEnded();

// While the pointer is locked, the cursor position is frozen by the browser,
// so the browser-provided movement deltas are used (and accumulated over the
// frame, as Firefox can report them across several events).
inputManager.onMouseMove(500, 600, { movementX: 4, movementY: -3 });
inputManager.onMouseMove(500, 600, { movementX: 1, movementY: 2 });
expect(inputManager.getMouseMovementX()).to.be(5);
expect(inputManager.getMouseMovementY()).to.be(-1);

inputManager.onFrameEnded();
expect(inputManager.getMouseMovementX()).to.be(0);
expect(inputManager.getMouseMovementY()).to.be(0);
});

it('should not report a spurious movement after the mouse re-enters the canvas', () => {
inputManager.onMouseEnter();
inputManager.onMouseMove(100, 100);
inputManager.onMouseMove(150, 130);
expect(inputManager.getMouseMovementX()).to.be(50);
expect(inputManager.getMouseMovementY()).to.be(30);
inputManager.onFrameEnded();

// Leaving then re-entering invalidates the reference position, so the next
// move doesn't report a large jump.
inputManager.onMouseLeave();
inputManager.onMouseEnter();
inputManager.onMouseMove(400, 500);
expect(inputManager.getMouseMovementX()).to.be(0);
expect(inputManager.getMouseMovementY()).to.be(0);
});

it('should simulate touch events from mouse events', () => {
inputManager.onMouseMove(500, 600);
expect(inputTools.hasAnyTouchOrMouseStarted(runtimeScene)).to.be(false);
Expand Down
Loading