From ddc2b8884dc43b4f25e49ea433b69e658097a589 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 28 May 2023 15:07:21 -0500 Subject: [PATCH] fix: [#2619] Camera rotation around the focus (#2620) Closes #2619 * Fixes bug where camera rotation wasn't working when `ex.Camera.rotation = ...` was set * Fixes an issue where `TileMap` offscreen culling didn't work when the camera was rotated, this was fixed by adjusting how the world bounds worked and testing tile bounds against that. * Fixes issue with general offscreen culling during camera rotation --- CHANGELOG.md | 1 + src/engine/Camera.ts | 7 +++ src/engine/Graphics/OffscreenSystem.ts | 8 ++- src/engine/Screen.ts | 13 +++-- src/engine/TileMap/TileMap.ts | 73 ++++++++++++++------------ src/spec/ActorSpec.ts | 1 + src/spec/CameraSpec.ts | 9 ++++ src/spec/GraphicsSystemSpec.ts | 2 + src/spec/OffscreenSystemSpec.ts | 1 + src/spec/SceneSpec.ts | 2 + src/spec/ScreenSpec.ts | 26 +++++++++ 11 files changed, 104 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9004b863..c219d25c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ are returned ### Fixed +- Fixed issue where `ex.Camera.rotation = ...` did not work to rotate the camera, also addressed offscreen culling issues that were revealed by this fix. - Fixed issue where the `ex.ScreenElement` anchor was not being accounted for properly when passed as a constructor parameter. - Fixed issue where you could not use multiple instances of Excalibur on the same page, you can now have as many Excalibur's as you want (up to the webgl context limit). - Fixed issue where `ex.ScreenElement` would log a warning when created without a height or width diff --git a/src/engine/Camera.ts b/src/engine/Camera.ts index b375ac96..606adbc8 100644 --- a/src/engine/Camera.ts +++ b/src/engine/Camera.ts @@ -769,7 +769,14 @@ export class Camera extends Class implements CanUpdate, CanInitialize { // Calculate camera transform this.transform.reset(); + this.transform.scale(this.zoom, this.zoom); + + // rotate about the focus + this.transform.translate(newCanvasWidth / 2, newCanvasHeight / 2); + this.transform.rotate(this.rotation); + this.transform.translate(-newCanvasWidth / 2, -newCanvasHeight / 2); + this.transform.translate(cameraPos.x, cameraPos.y); this.transform.inverse(this.inverse); } diff --git a/src/engine/Graphics/OffscreenSystem.ts b/src/engine/Graphics/OffscreenSystem.ts index 0edb3e93..1d7e60ff 100644 --- a/src/engine/Graphics/OffscreenSystem.ts +++ b/src/engine/Graphics/OffscreenSystem.ts @@ -1,6 +1,7 @@ import { GraphicsComponent } from './GraphicsComponent'; import { EnterViewPortEvent, ExitViewPortEvent } from '../Events'; import { Scene } from '../Scene'; +import { Screen } from '../Screen'; import { Entity } from '../EntityComponentSystem/Entity'; import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent'; import { Camera } from '../Camera'; @@ -8,18 +9,23 @@ import { System, SystemType } from '../EntityComponentSystem/System'; import { ParallaxComponent } from './ParallaxComponent'; import { Vector } from '../Math/vector'; import { CoordPlane } from '../Math/coord-plane'; +import { BoundingBox } from '../Collision/BoundingBox'; export class OffscreenSystem extends System { public readonly types = ['ex.transform', 'ex.graphics'] as const; public systemType = SystemType.Draw; priority: number = -1; private _camera: Camera; + private _screen: Screen; + private _worldBounds: BoundingBox; public initialize(scene: Scene): void { this._camera = scene.camera; + this._screen = scene.engine.screen; } update(entities: Entity[]): void { + this._worldBounds = this._screen.getWorldBounds(); let transform: TransformComponent; let graphics: GraphicsComponent; let maybeParallax: ParallaxComponent; @@ -59,7 +65,7 @@ export class OffscreenSystem extends System { engine.addScene('test', scene); engine.goToScene('test'); scene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); spyOn(scene, 'draw').and.callThrough(); spyOn(scene, 'debugDraw').and.callThrough(); diff --git a/src/spec/CameraSpec.ts b/src/spec/CameraSpec.ts index a235afe8..21ae5300 100644 --- a/src/spec/CameraSpec.ts +++ b/src/spec/CameraSpec.ts @@ -155,6 +155,15 @@ describe('A camera', () => { expect(Camera.y).toBe(555); }); + it('can be rotated', () => { + Camera._initialize(engine); + Camera.rotation = Math.PI / 2; + + Camera.updateTransform(); + + expect(Camera.transform.getRotation()).toBe(Math.PI / 2); + }); + it('can have its velocity set 2 ways', () => { Camera.dx = 100; Camera.dy = 1000; diff --git a/src/spec/GraphicsSystemSpec.ts b/src/spec/GraphicsSystemSpec.ts index c415a839..2db80912 100644 --- a/src/spec/GraphicsSystemSpec.ts +++ b/src/spec/GraphicsSystemSpec.ts @@ -41,6 +41,7 @@ describe('A Graphics ECS System', () => { const offscreenSystem = new ex.OffscreenSystem(); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); offscreenSystem.initialize(engine.currentScene); sut.initialize(engine.currentScene); @@ -205,6 +206,7 @@ describe('A Graphics ECS System', () => { const offscreenSystem = new ex.OffscreenSystem(); engine.currentScene.camera.update(engine, 1); engine.currentScene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); offscreenSystem.initialize(engine.currentScene); sut.initialize(engine.currentScene); diff --git a/src/spec/OffscreenSystemSpec.ts b/src/spec/OffscreenSystemSpec.ts index 0d611218..1fe4b250 100644 --- a/src/spec/OffscreenSystemSpec.ts +++ b/src/spec/OffscreenSystemSpec.ts @@ -28,6 +28,7 @@ describe('The OffscreenSystem', () => { it('decorates offscreen entities with "offscreen" tag', () => { const sut = new ex.OffscreenSystem(); engine.currentScene.camera.update(engine, 1); + engine.screen.setCurrentCamera(engine.currentScene.camera); engine.currentScene._initialize(engine); sut.initialize(engine.currentScene); diff --git a/src/spec/SceneSpec.ts b/src/spec/SceneSpec.ts index 1d474749..d152b818 100644 --- a/src/spec/SceneSpec.ts +++ b/src/spec/SceneSpec.ts @@ -762,6 +762,7 @@ describe('A scene', () => { it('can have onPreDraw overridden safely', () => { scene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); scene.onPreDraw = (ctx, delta) => { expect(ctx).not.toBe(null); expect(delta).toBe(100); @@ -779,6 +780,7 @@ describe('A scene', () => { it('can have onPostDraw overridden safely', () => { scene._initialize(engine); + engine.screen.setCurrentCamera(engine.currentScene.camera); scene.onPostDraw = (ctx, delta) => { expect(ctx).not.toBe(null); expect(delta).toBe(100); diff --git a/src/spec/ScreenSpec.ts b/src/spec/ScreenSpec.ts index 784e1f19..20902330 100644 --- a/src/spec/ScreenSpec.ts +++ b/src/spec/ScreenSpec.ts @@ -763,6 +763,32 @@ describe('A Screen', () => { expect(bounds.top).toBe(150); }); + it('can return world bounds with camera rotation', () => { + const sut = new ex.Screen({ + canvas, + context, + browser, + viewport: { width: 800, height: 600 }, + pixelRatio: 2 + }); + const camera = new Camera(); + camera.x = 400; + camera.y = 300; + camera.rotation = Math.PI / 2; + camera.zoom = 2; + + sut.setCurrentCamera(camera); + sut.applyResolutionAndViewport(); + camera._initialize({screen: sut, clock: { elapsed: () => 16}} as ex.Engine); + + const bounds = sut.getWorldBounds(); + + expect(bounds.left).toBe(250); + expect(bounds.right).toBe(550); + expect(bounds.bottom).toBe(500); + expect(bounds.top).toBe(100); + }); + it('can calculate screen center without a camera and no relevant pixel ratio', () => { const sut = new ex.Screen({ canvas, -- 2.51.2