diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index dd6545487b..cea1b3dbe6 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed position calculation in `HudMarginComponent` when using a viewport - Add noClip option to `FixedResolutionViewport` - Add a few missing helpers to SpriteAnimation + - Fix render order of components and add tests ## [1.0.0-releasecandidate.17] - Added `StandardEffectController` class diff --git a/packages/flame/lib/src/game/camera/camera_wrapper.dart b/packages/flame/lib/src/game/camera/camera_wrapper.dart index b8cf954c23..1e415879e3 100644 --- a/packages/flame/lib/src/game/camera/camera_wrapper.dart +++ b/packages/flame/lib/src/game/camera/camera_wrapper.dart @@ -22,33 +22,23 @@ class CameraWrapper { // TODO(st-pasha): it would be easier to keep the world and the // HUD as two separate component trees. camera.viewport.render(canvas, (_canvas) { - // First render regular world objects - canvas.save(); - camera.apply(canvas); + var hasCamera = false; // so we don't apply unecessary transformations world.forEach((component) { - if (!component.isHud) { - // TODO(st-pasha): refactor [ParallaxComponent] so that it - // wouldn't require any camera hacks. - if (component is ParallaxComponent) { - canvas.restore(); - } + if (!component.isHud && !hasCamera) { canvas.save(); - component.renderTree(canvas); - canvas.restore(); - if (component is ParallaxComponent) { - camera.apply(canvas); - } - } - }); - canvas.restore(); - // Then render the HUD - world.forEach((component) { - if (component.isHud) { - canvas.save(); - component.renderTree(canvas); + camera.apply(canvas); + hasCamera = true; + } else if (component.isHud && hasCamera) { canvas.restore(); + hasCamera = false; } + canvas.save(); + component.renderTree(canvas); + canvas.restore(); }); + if (hasCamera) { + canvas.restore(); + } }); } } diff --git a/packages/flame/pubspec.yaml b/packages/flame/pubspec.yaml index d3fffb3b9a..00f764e79f 100644 --- a/packages/flame/pubspec.yaml +++ b/packages/flame/pubspec.yaml @@ -20,7 +20,7 @@ dev_dependencies: test: ^1.17.10 dartdoc: ^0.42.0 mocktail: ^0.1.4 - canvas_test: ^0.1.1 + canvas_test: ^0.2.0 flame_test: path: ../flame_test flame_lint: diff --git a/packages/flame/test/components/position_component_test.dart b/packages/flame/test/components/position_component_test.dart index a41c7307b4..0df4cf141b 100644 --- a/packages/flame/test/components/position_component_test.dart +++ b/packages/flame/test/components/position_component_test.dart @@ -632,7 +632,8 @@ void main() { ..drawLine(const Offset(0, -2), const Offset(0, 2)) ..drawLine(const Offset(-2, 0), const Offset(2, 0)) ..drawParagraph(null, const Offset(-30, -15)) - ..drawParagraph(null, const Offset(-20, 10)), + ..drawParagraph(null, const Offset(-20, 10)) + ..translate(0, 0), // canvas.restore ); }); @@ -650,7 +651,8 @@ void main() { ..translate(18, 12) ..drawRect(const Rect.fromLTWH(0, 0, 10, 10)) ..drawLine(const Offset(5, 3), const Offset(5, 7)) - ..drawLine(const Offset(3, 5), const Offset(7, 5)), + ..drawLine(const Offset(3, 5), const Offset(7, 5)) + ..translate(0, 0), // canvas.restore ); }); }); diff --git a/packages/flame/test/game/camera_and_viewport_test.dart b/packages/flame/test/game/camera_and_viewport_test.dart index fc09bacd1e..19fcdeeef2 100644 --- a/packages/flame/test/game/camera_and_viewport_test.dart +++ b/packages/flame/test/game/camera_and_viewport_test.dart @@ -30,7 +30,7 @@ void main() { expect(game.size, Vector2(100.0, 200.00)); }); - flameGame.test('fixed ratio viewport has perfect ratio', (game) { + flameGame.test('fixed ratio viewport has perfect ratio', (game) async { game.camera.viewport = FixedResolutionViewport(Vector2.all(50)); game.onGameResize(Vector2.all(200.0)); expect(game.canvasSize, Vector2.all(200.00)); @@ -41,17 +41,21 @@ void main() { expect(viewport.scaledSize, Vector2(200.0, 200.0)); expect(viewport.scale, 4.0); + await game.ensureAdd(_TestComponent(Vector2.zero())); + final canvas = MockCanvas(); game.render(canvas); expect( canvas, MockCanvas() ..clipRect(const Rect.fromLTWH(0, 0, 200, 200)) - ..scale(4), + ..scale(4) + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); - flameGame.test('fixed ratio viewport maxes width', (game) { + flameGame.test('fixed ratio viewport maxes width', (game) async { game.camera.viewport = FixedResolutionViewport(Vector2.all(50)); game.onGameResize(Vector2(100.0, 200.0)); expect(game.canvasSize, Vector2(100.0, 200.00)); @@ -62,6 +66,8 @@ void main() { expect(viewport.scaledSize, Vector2(100.0, 100.0)); expect(viewport.scale, 2.0); + await game.ensureAdd(_TestComponent(Vector2.zero())); + final canvas = MockCanvas(); game.render(canvas); expect( @@ -69,11 +75,13 @@ void main() { MockCanvas() ..clipRect(const Rect.fromLTWH(0, 50, 100, 100)) ..translate(0, 50) - ..scale(2), + ..scale(2) + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); - flameGame.test('fixed ratio viewport maxes height', (game) { + flameGame.test('fixed ratio viewport maxes height', (game) async { game.camera.viewport = FixedResolutionViewport(Vector2(100.0, 400.0)); game.onGameResize(Vector2(100.0, 200.0)); expect(game.canvasSize, Vector2(100.0, 200.00)); @@ -84,6 +92,8 @@ void main() { expect(viewport.scaledSize, Vector2(50.0, 200.0)); expect(viewport.scale, 0.5); + await game.ensureAdd(_TestComponent(Vector2.zero())); + final canvas = MockCanvas(); game.render(canvas); expect( @@ -91,19 +101,19 @@ void main() { MockCanvas() ..clipRect(const Rect.fromLTWH(25, 0, 50, 200)) ..translate(25, 0) - ..scale(0.5), + ..scale(0.5) + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); }); group('camera', () { - flameGame.test('default camera applies no translation', (game) { + flameGame.test('default camera applies no translation', (game) async { game.onGameResize(Vector2.all(100.0)); expect(game.camera.position, Vector2.zero()); - final p = _TestComponent(Vector2.all(10.0)); - game.add(p); - game.update(0); + await game.ensureAdd(_TestComponent(Vector2.all(10.0))); final canvas = MockCanvas(); game.render(canvas); @@ -111,17 +121,16 @@ void main() { canvas, MockCanvas() ..translate(10, 10) - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); - flameGame.test('camera snap movement', (game) { + flameGame.test('camera snap movement', (game) async { game.onGameResize(Vector2.all(100.0)); expect(game.camera.position, Vector2.zero()); - final p = _TestComponent(Vector2.all(10.0)); - game.add(p); - game.update(0); + await game.ensureAdd(_TestComponent(Vector2.all(10.0))); // this puts the top left of the screen on (4,4) game.camera.moveTo(Vector2.all(4.0)); @@ -136,7 +145,8 @@ void main() { MockCanvas() ..translate(-4, -4) // Camera translation ..translate(10, 10) // PositionComponent translation - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); @@ -156,12 +166,11 @@ void main() { expect(game.camera.position, Vector2(0.0, 10.0)); }); - flameGame.test('camera follow', (game) { + flameGame.test('camera follow', (game) async { game.onGameResize(Vector2.all(100.0)); final p = _TestComponent(Vector2.all(10.0))..anchor = Anchor.center; - game.add(p); - game.update(0); + await game.ensureAdd(p); game.camera.followComponent(p); expect(game.camera.position, Vector2.all(0.0)); @@ -178,17 +187,17 @@ void main() { MockCanvas() ..translate(40, 30) // Camera translation ..translate(9.5, 19.5) // PositionComponent translation - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera // result: 50 - w/2, 50 - h/2 (perfectly centered) ); }); - flameGame.test('camera follow with relative position', (game) { + flameGame.test('camera follow with relative position', (game) async { game.onGameResize(Vector2.all(100.0)); final p = _TestComponent(Vector2.all(10.0))..anchor = Anchor.center; - game.add(p); - game.update(0); + await game.ensureAdd(p); // this would be a typical vertical shoot-em-up game.camera.followComponent(p, relativeOffset: const Anchor(0.5, 0.8)); @@ -206,15 +215,15 @@ void main() { MockCanvas() ..translate(-550, -1920) // Camera translation ..translate(599.5, 1999.5) // PositionComponent translation - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); - flameGame.test('camera follow with world boundaries', (game) { + flameGame.test('camera follow with world boundaries', (game) async { game.onGameResize(Vector2.all(100.0)); final p = _TestComponent(Vector2.all(10.0))..anchor = Anchor.center; - game.add(p); - game.update(0); + await game.ensureAdd(p); game.camera.followComponent( p, worldBounds: const Rect.fromLTWH(-1000, -1000, 2000, 2000), @@ -244,12 +253,11 @@ void main() { flameGame.test( 'camera follow with world boundaries smaller than the screen', - (game) { + (game) async { game.onGameResize(Vector2.all(200.0)); final p = _TestComponent(Vector2.all(10.0))..anchor = Anchor.center; - game.add(p); - game.update(0); + await game.ensureAdd(p); game.camera.followComponent( p, worldBounds: const Rect.fromLTWH(0, 0, 100, 100), @@ -281,13 +289,12 @@ void main() { expect(game.camera.position, Vector2.all(-100.0)); }); - flameGame.test('camera zoom', (game) { + flameGame.test('camera zoom', (game) async { game.onGameResize(Vector2.all(200.0)); game.camera.zoom = 2; final p = _TestComponent(Vector2.all(100.0))..anchor = Anchor.center; - game.add(p); - game.update(0); + await game.ensureAdd(p); final canvas = MockCanvas(); game.render(canvas); @@ -296,7 +303,8 @@ void main() { MockCanvas() ..scale(2) // Camera zoom ..translate(99.5, 99.5) // PositionComponent translation - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); }); @@ -317,7 +325,8 @@ void main() { ..translate(100, 100) // camera translation ..scale(2) // camera zoom ..translate(99.5, 99.5) // position component - ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)), + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..translate(0, 0), // reset camera ); expect(game.camera.position, Vector2.all(-50.0)); }); @@ -337,7 +346,7 @@ void main() { group('viewport & camera', () { flameGame.test( 'default ratio viewport + camera with world boundaries', - (game) { + (game) async { final game = FlameGame() ..camera.viewport = FixedResolutionViewport(Vector2.all(100)); game.onGameResize(Vector2.all(200.0)); @@ -345,7 +354,7 @@ void main() { expect(game.size, Vector2.all(100.00)); final p = _TestComponent(Vector2.all(10.0))..anchor = Anchor.center; - game.add(p); + await game.ensureAdd(p); game.camera.followComponent( p, // this could be a typical mario-like platformer, where the player is diff --git a/packages/flame/test/game/component_rendering_test.dart b/packages/flame/test/game/component_rendering_test.dart new file mode 100644 index 0000000000..283bf24a69 --- /dev/null +++ b/packages/flame/test/game/component_rendering_test.dart @@ -0,0 +1,108 @@ +import 'dart:ui'; + +import 'package:canvas_test/canvas_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _MyComponent extends Component { + @override + bool isHud; + + _MyComponent(int priority, {this.isHud = false}) : super(priority: priority); + + @override + void render(Canvas canvas) { + final p = Vector2Extension.fromInts(priority, priority); + final s = Vector2.all(1.0); + canvas.drawRect(p & s, BasicPalette.white.paint()); + } +} + +void main() { + group('components are rendered according to their priorities', () { + flameGame.test( + 'only camera components', + (game) async { + await game.ensureAddAll([ + _MyComponent(4), + _MyComponent(1), + _MyComponent(2), + ]); + + final canvas = MockCanvas(); + game.camera.snapTo(Vector2(12.0, 18.0)); + game.render(canvas); + + expect( + canvas, + MockCanvas() + ..translate(-12.0, -18.0) + ..drawRect(const Rect.fromLTWH(1, 1, 1, 1)) + ..drawRect(const Rect.fromLTWH(2, 2, 1, 1)) + ..drawRect(const Rect.fromLTWH(4, 4, 1, 1)) + ..translate(0.0, 0.0), + ); + }, + ); + + flameGame.test( + 'only HUD components', + (game) async { + await game.ensureAddAll([ + _MyComponent(4, isHud: true), + _MyComponent(1, isHud: true), + _MyComponent(2, isHud: true), + ]); + final canvas = MockCanvas(); + game.camera.snapTo(Vector2(12.0, 18.0)); + game.render(canvas); + + expect( + canvas, + MockCanvas() + ..drawRect(const Rect.fromLTWH(1, 1, 1, 1)) + ..drawRect(const Rect.fromLTWH(2, 2, 1, 1)) + ..drawRect(const Rect.fromLTWH(4, 4, 1, 1)), + ); + }, + ); + + flameGame.test( + 'mixed', + (game) async { + await game.ensureAddAll([ + _MyComponent(4), + _MyComponent(1), + _MyComponent(2, isHud: true), + _MyComponent(5, isHud: true), + _MyComponent(3, isHud: true), + _MyComponent(0), + ]); + + final canvas = MockCanvas(); + game.camera.snapTo(Vector2(12.0, 18.0)); + game.render(canvas); + + expect( + canvas, + MockCanvas() + ..translate(-12.0, -18.0) + ..drawRect(const Rect.fromLTWH(0, 0, 1, 1)) + ..drawRect(const Rect.fromLTWH(1, 1, 1, 1)) + ..translate(0.0, 0.0) + ..drawRect(const Rect.fromLTWH(2, 2, 1, 1)) + ..drawRect(const Rect.fromLTWH(3, 3, 1, 1)) + ..translate(-12.0, -18.0) + ..drawRect(const Rect.fromLTWH(4, 4, 1, 1)) + ..translate(0.0, 0.0) + ..drawRect(const Rect.fromLTWH(5, 5, 1, 1)), + ); + }, + ); + }); +}