Skip to content

Commit

Permalink
feat!: Add CameraComponent to FlameGame (#2740)
Browse files Browse the repository at this point in the history
This PR adds the `CameraComponent` and a `World` by default to the
FlameGame.
  • Loading branch information
spydon committed Sep 18, 2023
1 parent 5be6fc8 commit 7c2f400
Show file tree
Hide file tree
Showing 89 changed files with 1,107 additions and 821 deletions.
9 changes: 7 additions & 2 deletions doc/flame/camera_and_viewport.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Camera and Viewport
# Camera and Viewport (Deprecated)

```{note}
This document describes the deprecated Camera API. The new `CameraComponent` approach
is described in [](camera_component.md).
```

When rendering on Flutter, the regular coordinate space used are logical pixels. That means one
pixel for Flutter is already not necessarily one real pixel on the device, because of the [device's
Expand Down Expand Up @@ -104,7 +109,7 @@ class MyGame extends FlameGame {
final someVector = Vector2(100, 100);
@override
void onLoad() {
camera.followVector2(someVector);
oldCamera.followVector2(someVector);
}
}
Expand Down
54 changes: 46 additions & 8 deletions doc/flame/camera_component.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

```{note}
This document describes a new camera API. The more traditional approach
(which will be deprecated) for handling a camera is described in
(which is deprecated) for handling a camera is described in
[](camera_and_viewport.md).
```

Camera-as-a-component is an alternative way of structuring a game, an approach
that allows more flexibility in placing the camera, or even having more than
one camera simultaneously.
Camera-as-a-component is the new way of structuring a game, an approach that
allows more flexibility in placing the camera, or even having more than one
camera simultaneously.

In order to understand how this approach works, imagine that your game world is
an entity that exists *somewhere* independently from your application. Imagine
Expand All @@ -33,8 +33,10 @@ the viewing location and angle.

This component should be used to host all other components that comprise your
game world. The main property of the `World` class is that it does not render
through traditional means -- instead, create one or more [](#cameracomponent)s
to "look at" the world.
through traditional means -- instead it is rendered by one or more
[](#cameracomponent)s to "look at" the world. In the `FlameGame` class there is
one `World` called `world` which is added by default and paired together with
the default `CameraComponent` called `camera`.

A game can have multiple `World` instances that can be rendered either at the
same time, or at different times. For example, if you have two worlds A and B
Expand All @@ -46,6 +48,23 @@ Just like with most `Component`s, children can be added to `World` by using the
`children` argument in its constructor, or by using the `add` or `addAll`
methods.

For many games you want to extend the world and create your logic in there,
such a game structure could look like this:

```dart
void main() {
runApp(GameWidget(FlameGame(world: MyWorld())));
}
class MyWorld extends World {
@override
Future<void> onLoad() async {
// Load all the assets that are needed in this world
// and add components etc.
}
}
```


## CameraComponent

Expand All @@ -54,6 +73,10 @@ reference to a `World` instance during construction; however later the target
world can be replaced with another one. Multiple cameras can observe the same
world at the same time.

There is a default `CameraComponent` called `camera` on the `FlameGame` class
which is paired together with the default `world`, so you don't need to create
or add your own `CameraComponent` if your game doesn't need to.

A `CameraComponent` has two other components inside: a [](#viewport) and a
[](#viewfinder). Unlike the `World` object, the camera owns the viewport and
the viewfinder, which means those components are children of the camera.
Expand All @@ -65,6 +88,21 @@ cases where the rendering of a component depends on the camera settings. For
example, some components may decide to skip rendering themselves and their
children if they are outside of the camera's viewport.

The `FlameGame` class has a `camera` field in its constructor, so you can set
what type of default camera that you want like this for example:

```dart
void main() {
runApp(
GameWidget(
FlameGame(
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
),
),
);
}
```


### CameraComponent.withFixedResolution()

Expand Down Expand Up @@ -191,9 +229,9 @@ if (!camera.canSee(component)) {
```


## Comparison to the traditional camera
## Comparison to the deprecated camera

Compared to the normal [Camera](camera_and_viewport.md), the `CameraComponent`
Compared to the deprecated [Camera](camera_and_viewport.md), the `CameraComponent`
has several advantages:

- Multiple cameras can be added to the game at the same time;
Expand Down
4 changes: 2 additions & 2 deletions doc/flame/flame.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
- [Platforms](platforms.md)
- [Collision Detection](collision_detection.md)
- [Effects](effects.md)
- [Camera & Viewport](camera_and_viewport.md)
- [Camera Component](camera_component.md)
- [Inputs](inputs/inputs.md)
- [Rendering](rendering/rendering.md)
- [Layout](layout/layout.md)
- [Overlays](overlays.md)
- [Other](other/other.md)
- [Camera & Viewport (deprecated)](camera_and_viewport.md)

```{toctree}
:hidden:
Expand All @@ -27,11 +27,11 @@ Router <router.md>
Platforms <platforms.md>
Collision Detection <collision_detection.md>
Effects <effects.md>
Camera & Viewport <camera_and_viewport.md>
Camera Component <camera_component.md>
Inputs <inputs/inputs.md>
Rendering <rendering/rendering.md>
Layout <layout/layout.md>
Overlays <overlays.md>
Other <other/other.md>
Camera & Viewport <camera_and_viewport.md>
```
38 changes: 16 additions & 22 deletions doc/flame/game.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# FlameGame

`FlameGame` is the most commonly used `Game` class in Flame.

The `FlameGame` class implements a `Component` based `Game`. It has a tree of components
and calls the `update` and `render` methods of all components that have been added to the game.

We refer to this component-based system as the Flame Component System (FCS). Throughout the
We refer to this component-based system as the Flame Component System (FCS). Throughout the
documentation, FCS is used to reference this system.

Components can be added to the `FlameGame` directly in the constructor with the named `children`
argument, or from anywhere else with the `add`/`addAll` methods.
argument, or from anywhere else with the `add`/`addAll` methods. Most of the time however, you want
to add your children to a `World`, the default world exist under `FlameGame.world` and you add
components to it just like you would to any other component.

A simple `FlameGame` implementation that adds two components, one in `onLoad` and one directly in
the constructor can look like this:
Expand All @@ -29,19 +29,17 @@ class MyCrate extends SpriteComponent {
}
}
class MyGame extends FlameGame {
class MyWorld extends World {
@override
Future<void> onLoad() async {
await add(MyCrate());
}
}
void main() {
final myGame = MyGame();
final myGame = FlameGame(world: MyWorld());
runApp(
GameWidget(
game: myGame,
),
GameWidget(game: myGame),
);
}
```
Expand All @@ -56,7 +54,7 @@ constructor.

To remove components from the list on a `FlameGame` the `remove` or `removeAll` methods can be used.
The first can be used if you just want to remove one component, and the second can be used when you
want to remove a list of components.
want to remove a list of components. These methods exist on all `Component`s, including the world.


## Game Loop
Expand All @@ -77,13 +75,8 @@ Every time the game needs to be resized, for example when the orientation is cha
will call all of the `Component`s `onGameResize` methods and it will also pass this information to
the camera and viewport.

The `FlameGame.camera` controls which point in the coordinate space should be the top-left of the
screen (it defaults to [0,0] like a regular `Canvas`).

```{note}
Utilizing `FlameGame.camera.gameSize` in the `onGameResize` event should be done
after the call to `super.onGameResize(canvasSize);`.
```
The `FlameGame.camera` controls which point in the coordinate space that should be at the anchor of
your viewfinder, [0,0] is in the center (`Anchor.center`) of the viewport by default.


## Lifecycle
Expand Down Expand Up @@ -178,8 +171,8 @@ class MyGame extends FlameGame with SingleGameInstance {
```{include} diagrams/low_level_game_api.md
```

The `Game` class is a low-level API that can be used when you want to implement the functionality of
how the game engine should be structured. `Game` does not implement any `update` or
The abstract `Game` class is a low-level API that can be used when you want to implement the
functionality of how the game engine should be structured. `Game` does not implement any `update` or
`render` function for example.

The class also has the lifecycle methods `onLoad`, `onMount` and `onRemove` in it, which are
Expand Down Expand Up @@ -226,9 +219,10 @@ A Flame `Game` can be paused and resumed in two ways:
- With the use of the `pauseEngine` and `resumeEngine` methods.
- By changing the `paused` attribute.

When pausing a Flame `Game`, the `GameLoop` is effectively paused, meaning that no updates or new
renders will happen until it is resumed.
When pausing a `Game`, the `GameLoop` is effectively paused, meaning that no updates or new renders
will happen until it is resumed.

While the game is paused, it is possible to advanced it frame by frame using the `stepEngine` method.
While the game is paused, it is possible to advanced it frame by frame using the `stepEngine`
method.
It might not be much useful in the final game, but can be very helpful in inspecting game state step
by step during the development cycle.
9 changes: 2 additions & 7 deletions doc/tutorials/platformer/app/lib/ember_quest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ class EmberQuestGame extends FlameGame
double cloudSpeed = 0.0;
double objectSpeed = 0.0;

final world = World();
late final CameraComponent cameraComponent;

@override
Future<void> onLoad() async {
//debugMode = true; // Uncomment to see the bounding boxes
Expand All @@ -39,9 +36,7 @@ class EmberQuestGame extends FlameGame
'star.png',
'water_enemy.png',
]);
cameraComponent = CameraComponent(world: world);
cameraComponent.viewfinder.anchor = Anchor.topLeft;
addAll([cameraComponent, world]);
camera.viewfinder.anchor = Anchor.topLeft;

initializeGame(loadHud: true);
}
Expand Down Expand Up @@ -112,7 +107,7 @@ class EmberQuestGame extends FlameGame
);
world.add(_ember);
if (loadHud) {
cameraComponent.viewport.add(Hud());
camera.viewport.add(Hud());
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/games/padracing/lib/padracing_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
@override
Future<void> onLoad() async {
super.onLoad();
cameraComponent.removeFromParent();
camera.removeFromParent();
children.register<CameraComponent>();

final walls = createWalls(trackSize);
Expand Down
4 changes: 4 additions & 0 deletions examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:examples/stories/layout/layout.dart';
import 'package:examples/stories/parallax/parallax.dart';
import 'package:examples/stories/rendering/rendering.dart';
import 'package:examples/stories/sprites/sprites.dart';
import 'package:examples/stories/structure/structure.dart';
import 'package:examples/stories/svg/svg.dart';
import 'package:examples/stories/system/system.dart';
import 'package:examples/stories/tiled/tiled.dart';
Expand Down Expand Up @@ -71,6 +72,9 @@ void runAsDashbook() {
// Some small sample games
addGameStories(dashbook);

// Show some different ways of structuring games
addStructureStories(dashbook);

// Feature examples
addAudioStories(dashbook);
addAnimationStories(dashbook);
Expand Down
7 changes: 1 addition & 6 deletions examples/lib/stories/animations/benchmark_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ starts to drop in FPS, this is without any sprite batching and such.
final counterPrefix = 'Animations: ';
final Random random = Random();

final world = World();
late final CameraComponent cameraComponent;

@override
Future<void> onLoad() async {
cameraComponent = CameraComponent(world: world);
addAll([cameraComponent, world]);
await cameraComponent.viewport.addAll([
await camera.viewport.addAll([
FpsTextComponent(
position: size - Vector2(0, 50),
anchor: Anchor.bottomRight,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BlobWorld extends Forge2DWorld
with TapCallbacks, HasGameReference<Forge2DGame> {
@override
Future<void> onLoad() async {
super.onLoad();
await super.onLoad();
final blobCenter = Vector2(0, -30);
final blobRadius = Vector2.all(6.0);
addAll(createBoundaries(game));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class CameraExampleWorld extends DominoExampleWorld {
final position = info.localPosition;
final pizza = Pizza(position);
add(pizza);
pizza.mounted.whenComplete(() => game.cameraComponent.follow(pizza));
pizza.mounted.whenComplete(() => game.camera.follow(pizza));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PulleyJointExample extends Forge2DGame {
@override
Future<void> onLoad() async {
super.onLoad();
final distanceFromCenter = cameraComponent.visibleWorldRect.width / 5;
final distanceFromCenter = camera.visibleWorldRect.width / 5;

final firstPulley = Ball(
Vector2(-distanceFromCenter, -10),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ class RaycastExample extends Forge2DGame with MouseMovementDetector {
void onMouseMove(PointerHoverInfo info) {
final rayStart = screenToWorld(
Vector2(
cameraComponent.viewport.size.x / 4,
cameraComponent.viewport.size.y / 2,
camera.viewport.size.x / 4,
camera.viewport.size.y / 2,
),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';

List<Wall> createBoundaries(Forge2DGame game, {double? strokeWidth}) {
final visibleRect = game.cameraComponent.visibleWorldRect;
final visibleRect = game.camera.visibleWorldRect;
final topLeft = visibleRect.topLeft.toVector2();
final topRight = visibleRect.topRight.toVector2();
final bottomRight = visibleRect.bottomRight.toVector2();
Expand Down

0 comments on commit 7c2f400

Please sign in to comment.