Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Backdrop (static backgrounds) component for CameraComponent #2787

Merged
merged 8 commits into from
Oct 3, 2023
Merged
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
9 changes: 9 additions & 0 deletions doc/flame/camera_component.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading