Skip to content

Commit

Permalink
feat: Backdrop (static backgrounds) component for CameraComponent (#2787
Browse files Browse the repository at this point in the history
)

This PR adds a backdrop component to the `CameraComponent` which renders
behind the world.
With this change the rendering order looks like this:
```
backdrop
world
viewport
viewfinder
```

In the following example you can see a `ParallaxComponent` attached to
the backdrop, a player and some other components attached to the world,
and some texts attached to the viewfinder and viewport:

![image](https://github.com/flame-engine/flame/assets/744771/a0b2fe0e-bff3-4d1d-a2d1-ddd3199bbdab)
  • Loading branch information
spydon committed Oct 3, 2023
1 parent 46d6269 commit ab329f7
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 29 deletions.
9 changes: 9 additions & 0 deletions doc/flame/camera_component.md
Expand Up @@ -28,6 +28,15 @@ Then, a [](#cameracomponent) class that "looks at" the `World`. The
flexibility of rendering the world at any place on the screen, and also control
the viewing location and angle.

If you add children to the `Viewport` they will appear as static HUDs in
front of the world and if you add children to the `Viewfinder` they will appear
statically in front of the viewport.

To add static components behind the world you can add them to the `backdrop`
component, or replace the `backdrop` component. This is for example useful if
you want to have a static `ParallaxComponent` beneath a world that you can move
around it.


## World

Expand Down
16 changes: 16 additions & 0 deletions examples/lib/stories/camera_and_viewport/camera_and_viewport.dart
Expand Up @@ -6,6 +6,7 @@ import 'package:examples/stories/camera_and_viewport/camera_follow_and_world_bou
import 'package:examples/stories/camera_and_viewport/coordinate_systems_example.dart';
import 'package:examples/stories/camera_and_viewport/fixed_resolution_example.dart';
import 'package:examples/stories/camera_and_viewport/follow_component_example.dart';
import 'package:examples/stories/camera_and_viewport/static_components_example.dart';
import 'package:examples/stories/camera_and_viewport/zoom_example.dart';
import 'package:flame/game.dart';

Expand Down Expand Up @@ -56,6 +57,21 @@ void addCameraAndViewportStories(Dashbook dashbook) {
codeLink: baseLink('camera_and_viewport/fixed_resolution_example.dart'),
info: FixedResolutionExample.description,
)
..add(
'HUDs and static components',
(context) {
return GameWidget(
game: StaticComponentsExample(
viewportResolution: Vector2(
context.numberProperty('viewport width', 500),
context.numberProperty('viewport height', 500),
),
),
);
},
codeLink: baseLink('camera_and_viewport/static_components_example.dart'),
info: StaticComponentsExample.description,
)
..add(
'Coordinate Systems',
(context) => const CoordinateSystemsWidget(),
Expand Down
@@ -0,0 +1,141 @@
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/parallax.dart';

class StaticComponentsExample extends FlameGame
with ScrollDetector, ScaleDetector {
static const description = '''
This example shows a parallax which is attached to the viewport (behind the
world), four Flame logos that are added to the world, and a player added to
the world which is also followed by the camera when you click somewhere.
The text components that are added are self-explanatory.
''';

late final ParallaxComponent myParallax;

StaticComponentsExample({
required Vector2 viewportResolution,
}) : super(
camera: CameraComponent.withFixedResolution(
width: viewportResolution.x,
height: viewportResolution.y,
),
world: _StaticComponentWorld(),
);

@override
Future<void> onLoad() async {
myParallax = MyParallaxComponent()..parallax?.baseVelocity.setZero();
camera.backdrop.addAll([
myParallax,
TextComponent(
text: 'Center backdrop Component',
position: camera.viewport.size / 2 + Vector2(0, 30),
anchor: Anchor.center,
),
]);
camera.viewfinder.addAll([
TextComponent(
text: 'Corner Viewfinder Component',
position: camera.viewport.size - Vector2.all(10),
anchor: Anchor.bottomRight,
),
]);
camera.viewport.addAll(
[
TextComponent(
text: 'Corner Viewport Component',
position: Vector2.all(10),
),
TextComponent(
text: 'Center Viewport Component',
position: camera.viewport.size / 2,
anchor: Anchor.center,
),
],
);
}
}

class _StaticComponentWorld extends World
with
HasGameReference<StaticComponentsExample>,
TapCallbacks,
DoubleTapCallbacks {
late SpriteComponent player;
@override
Future<void> onLoad() async {
final playerSprite = await game.loadSprite('layers/player.png');
final flameSprite = await game.loadSprite('flame.png');
final visibleSize = game.camera.visibleWorldRect.toVector2();
add(player = SpriteComponent(sprite: playerSprite, anchor: Anchor.center));
addAll([
SpriteComponent(
sprite: flameSprite,
anchor: Anchor.center,
position: -visibleSize / 8,
size: Vector2(20, 30),
),
SpriteComponent(
sprite: flameSprite,
anchor: Anchor.center,
position: visibleSize / 8,
size: Vector2(20, 30),
),
SpriteComponent(
sprite: flameSprite,
anchor: Anchor.center,
position: (visibleSize / 8)..multiply(Vector2(-1, 1)),
size: Vector2(20, 30),
),
SpriteComponent(
sprite: flameSprite,
anchor: Anchor.center,
position: (visibleSize / 8)..multiply(Vector2(1, -1)),
size: Vector2(20, 30),
),
]);
game.camera.follow(player, maxSpeed: 100);
}

@override
void onTapDown(TapDownEvent event) {
const moveDuration = 1.0;
final deltaX = (event.localPosition - player.position).x;
player.add(
MoveToEffect(
event.localPosition,
EffectController(
duration: moveDuration,
),
onComplete: () => game.myParallax.parallax?.baseVelocity.setZero(),
),
);
final moveSpeedX = deltaX / moveDuration;
game.myParallax.parallax?.baseVelocity.setValues(moveSpeedX, 0);
}
}

class MyParallaxComponent extends ParallaxComponent {
@override
Future<void> onLoad() async {
parallax = await game.loadParallax(
[
ParallaxImageData('parallax/bg.png'),
ParallaxImageData('parallax/mountain-far.png'),
ParallaxImageData('parallax/mountains.png'),
ParallaxImageData('parallax/trees.png'),
ParallaxImageData('parallax/foreground-trees.png'),
],
baseVelocity: Vector2(0, 0),
velocityMultiplierDelta: Vector2(1.8, 1.0),
filterQuality: FilterQuality.none,
);
}
}
28 changes: 23 additions & 5 deletions packages/flame/lib/src/camera/camera_component.dart
Expand Up @@ -45,15 +45,17 @@ class CameraComponent extends Component {
this.world,
Viewport? viewport,
Viewfinder? viewfinder,
Component? backdrop,
List<Component>? hudComponents,
}) : _viewport = (viewport ?? MaxViewport())..addAll(hudComponents ?? []),
_viewfinder = viewfinder ?? Viewfinder(),
_backdrop = backdrop ?? Component(),
// The priority is set to the max here to avoid some bugs for the users,
// if they for example would add any components that modify positions
// before the CameraComponent, since it then will render the positions
// of the last tick each tick.
super(priority: 0x7fffffff) {
addAll([_viewport, _viewfinder]);
addAll([_backdrop, _viewport, _viewfinder]);
}

/// Create a camera that shows a portion of the game world of fixed size
Expand All @@ -69,13 +71,15 @@ class CameraComponent extends Component {
required double width,
required double height,
World? world,
Component? backdrop,
List<Component>? hudComponents,
}) {
return CameraComponent(
world: world,
viewport: FixedAspectRatioViewport(aspectRatio: width / height)
..addAll(hudComponents ?? []),
viewfinder: Viewfinder()..visibleGameSize = Vector2(width, height),
backdrop: backdrop,
);
}

Expand Down Expand Up @@ -122,6 +126,18 @@ class CameraComponent extends Component {
/// this variable is a mere reference to it.
World? world;

/// The [backdrop] component is rendered statically behind the world.
///
/// Here you can add things like the parallax component which should be static
/// when the camera moves around.
Component get backdrop => _backdrop;
Component _backdrop;
set backdrop(Component newBackdrop) {
_backdrop.removeFromParent();
add(newBackdrop);
_backdrop = newBackdrop;
}

/// The axis-aligned bounding rectangle of a [world] region which is currently
/// visible through the viewport.
///
Expand Down Expand Up @@ -150,15 +166,16 @@ class CameraComponent extends Component {

/// Renders the [world] as seen through this camera.
///
/// If the world is not mounted yet, only the viewport HUD elements will be
/// rendered.
/// If the world is not mounted yet, only the viewport and viewfinder elements
/// will be rendered.
@override
void renderTree(Canvas canvas) {
canvas.save();
canvas.translate(
viewport.position.x - viewport.anchor.x * viewport.size.x,
viewport.position.y - viewport.anchor.y * viewport.size.y,
);
backdrop.renderTree(canvas);
// Render the world through the viewport
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
Expand All @@ -168,14 +185,15 @@ class CameraComponent extends Component {
currentCameras.add(this);
canvas.transform(viewfinder.transform.transformMatrix.storage);
world!.renderFromCamera(canvas);
viewfinder.renderTree(canvas);
} finally {
currentCameras.removeLast();
}
canvas.restore();
}
// Now render the HUD elements
// Render the viewport elements, which will be in front of the world.
viewport.renderTree(canvas);
// Render the viewfinder elements, which will be in front of the viewport.
viewfinder.renderTree(canvas);
canvas.restore();
}

Expand Down
3 changes: 3 additions & 0 deletions packages/flame/lib/src/camera/viewfinder.dart
Expand Up @@ -15,6 +15,9 @@ import 'package:vector_math/vector_math_64.dart';
/// The viewfinder contains the game point that is currently at the
/// "cross-hairs" of the viewport ([position]), the [zoom] level, and the
/// [angle] of rotation of the camera.
///
/// If you add children to the [Viewfinder] they will appear like HUDs i.e.
/// statically in front of the world.
class Viewfinder extends Component
implements AnchorProvider, AngleProvider, PositionProvider, ScaleProvider {
/// Internal transform matrix used by the viewfinder.
Expand Down
Expand Up @@ -31,7 +31,9 @@ class FixedAspectRatioViewport extends Viewport {

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
if (isLoaded) {
super.onGameResize(size);
}
_handleResize(size);
}

Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test3.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 39 additions & 1 deletion packages/flame/test/camera/camera_component_test.dart
@@ -1,13 +1,15 @@
import 'dart:math';
import 'dart:ui';

import 'package:flame/camera.dart';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'camera_test_helpers.dart';

void main() {
group('CameraComponent', () {
testGolden(
Expand Down Expand Up @@ -305,6 +307,42 @@ void main() {

expect(camera.canSee(component), isFalse);
});

testGolden(
'Correct order of rendering',
(game) async {
final world = World();
final camera = CameraComponent(world: world);
game.addAll([world, camera]);
camera.viewfinder.position = Vector2.all(4);
camera.backdrop.add(
CrossHair(
size: Vector2.all(28),
position: camera.viewport.size / 2 + Vector2.all(4),
color: Colors.teal,
),
);
camera.viewfinder.add(
CrossHair(
size: Vector2.all(20),
position: camera.viewport.size / 2 + Vector2(-6, 0),
color: Colors.white,
),
);
world.add(
CrossHair(size: Vector2.all(14), color: Colors.green),
);
camera.viewport.add(
CrossHair(
size: Vector2.all(8),
position: camera.viewport.size / 2 + Vector2(4, -4),
color: Colors.red,
),
);
},
goldenFile: '../_goldens/camera_component_order_test.png',
size: Vector2(50, 50),
);
});

group('CameraComponent.canSee', () {
Expand Down
20 changes: 20 additions & 0 deletions packages/flame/test/camera/camera_test_helpers.dart
@@ -0,0 +1,20 @@
import 'dart:ui';

import 'package:flame/components.dart';

class CrossHair extends PositionComponent {
CrossHair({super.size, super.position, this.color = const Color(0xFFFF0000)})
: super(anchor: Anchor.center);

final Color color;
Paint get _paint => Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = color;

@override
void render(Canvas canvas) {
canvas.drawLine(Offset(size.x / 2, 0), Offset(size.x / 2, size.y), _paint);
canvas.drawLine(Offset(0, size.y / 2), Offset(size.x, size.y / 2), _paint);
}
}

0 comments on commit ab329f7

Please sign in to comment.