Skip to content

Commit

Permalink
feat: BodyComponent can properly have normal Flame component childr…
Browse files Browse the repository at this point in the history
…en (#1442)
  • Loading branch information
spydon committed Mar 13, 2022
1 parent e01b4b1 commit 7fe8b6d
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 24 deletions.
62 changes: 60 additions & 2 deletions doc/flame/camera_and_viewport.md
Expand Up @@ -85,14 +85,72 @@ When dealing with input events, it is imperative to convert screen coordinates t
(or, for some reasons, you might want to do the reverse). The Camera provides two functions,
`screenToWorld` and `worldToScreen` to easily convert between these coordinate spaces.


### Camera.followVector2

Immediately snaps the camera to start following a `Vector2`.

This means that the camera will move so that the position vector is in a fixed position on the
screen. That position is determined by a fraction of screen size defined by the `relativeOffset`
argument (defaults to the center). The `worldBounds` argument can be optionally set to add
boundaries to how far the camera is allowed to move.

Example:

```dart
class MyGame extends FlameGame {
final someVector = Vector2(100, 100);
Future<void> onLoad() async {
camera.followVector2(someVector);
}
}
```


### Camera.followComponent

Immediately snaps the camera to start following a `PositionComponent`.

This means that the camera will move so that the position vector of the component is in a fixed
position on the screen. That position is determined by a fraction of screen size defined by the
`relativeOffset` argument (defaults to the center).
The `worldBounds` argument can be optionally set to add boundaries to how far the camera is allowed
to move.

The component is "grabbed" by its anchor (default top left).
So for example if you want the center of the object to be at the fixed position, set the components
anchor to center.

Example:

```dart
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final sprite = await loadSprite('pizza.png');
final player = SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
);
add(player);
camera.followComponent(player);
}
}
```


### Using the camera with the Game class

If you are not using `FlameGame`, but instead are using the `Game` class, then you need to manage
If you are not using `FlameGame`, but instead are using the `Game` mixin, then you need to manage
calling certain camera methods yourself. Let's say we have the following game structure, and we
want to add the camera functionality:

```dart
class YourGame extends Game {
class YourGame with Game {
Camera? camera;
Future<void> onLoad() async {}
Expand Down
29 changes: 25 additions & 4 deletions doc/other_modules/forge2d.md
Expand Up @@ -13,10 +13,10 @@ in the
[Forge2D example](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example)
and in the pub.dev [installation instructions](https://pub.dev/packages/flame_forge2d).

## Forge2DGame (FlameGame extension)
## Forge2DGame

If you are going to use Forge2D in your project it can be a good idea to use the Forge2D specific
extension of the `FlameGame` class.
`FlameGame` class, `Forge2DGame`.

It is called `Forge2DGame` and it will control the adding and removal of Forge2D's `BodyComponents`
as well as your normal components.
Expand All @@ -38,6 +38,7 @@ physics engine to properly update every child.
A simple `Forge2DGame` implementation examples can be seen in the
[examples folder](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d/example).


## BodyComponent

If you don't need to have a sprite on top of your body you should use the plain `BodyComponent`, for
Expand All @@ -48,20 +49,31 @@ anything after you have created a `Body` and added the `BodyComponent` to the ga
turn it off you can either override `debugMode` to set it to false or assign false to it in your
component constructor.


## SpriteBodyComponent

```{warning}
Deprecated: add a `SpriteComponent` to a [#BodyComponent] instead. Will be removed in 0.10.0
```

Often you want to render a sprite on top of the `BodyComponent` that you are going to use in your
`Forge2DGame`. This component will handle the scaling and positioning of your sprite on top of the
body.


## PositionBodyComponent

```{warning}
Deprecated: add children to a [#BodyComponent] instead. Will be removed in 0.10.0
```

One of the most commonly used classes in Flame is the `PositionComponent`, many of the commonly used
components in Flame are subclasses of `PositionComponent`. If you want to put a `PositionComponent`
or any of its subclasses on top of a Forge2D body you can use the `PositionBodyComponent` and it
will, just like with the `SpriteBodyComponent`, handle the rotation, positioning and scaling of that
component so that it follows the underlying `Body`.


## Contact callbacks

If you are using `Forge2DGame` you can take advantage of its way of handling contacts between two
Expand Down Expand Up @@ -116,10 +128,11 @@ parameters of your `ContactCallback` like this:
An implementation example can be seen in the
[Flame Forge2D example](https://github.com/flame-engine/flame_forge2d/blob/main/example).


## Viewport and Camera

`Forge2DGame` is using an implementation of the normal Flame `Viewport` and `Camera`, which can be
read more about [here](../flame/camera_and_viewport.md).
`Forge2DGame` is using its own implementation of the normal Flame `Viewport` and `Camera`, which can
be read more about [here](../flame/camera_and_viewport.md).

If you see your screen as a window and the outside as the Forge2D world, then the `Viewport` is the
part of the world outside that you can see through the window, so the parts that you can see on
Expand All @@ -128,3 +141,11 @@ your screen.
To move around what you see in that window you use the `Camera`, which can also be very useful if
you want to follow one of your components around in the Forge2D world, or know where in the world
a point on the screen is or vice versa.


### Forge2DCamera.followBodyComponent

Just like with normal `PositionComponent`s you can make the `Forge2DCamera` follow `BodyComponent`s
by calling `camera.followBodyComponent(...)` which works the same as
[camera.followComponent](../flame/camera_and_viewport.md#Camera.followComponent). When you want to
stop following a `BodyComponent` you should call `camera.unfollowBodyComponent`.
3 changes: 1 addition & 2 deletions packages/flame_forge2d/example/lib/camera_sample.dart
Expand Up @@ -9,7 +9,6 @@ class CameraSample extends DominoSample {
final position = details.eventPosition.game;
final pizza = Pizza(position);
add(pizza);
final component = pizza.positionComponent;
camera.followComponent(component!);
pizza.mounted.whenComplete(() => camera.followBodyComponent(pizza));
}
}
2 changes: 1 addition & 1 deletion packages/flame_forge2d/example/lib/composition_sample.dart
Expand Up @@ -31,7 +31,7 @@ class TapableBall extends Ball with Tappable {
late final TextPaint _textPaint;

TapableBall(Vector2 position) : super(position) {
originalPaint = BasicPalette.white.paint();
originalPaint = Paint()..color = Colors.amber;
paint = originalPaint;
}

Expand Down
9 changes: 7 additions & 2 deletions packages/flame_forge2d/example/lib/position_body_sample.dart
Expand Up @@ -6,13 +6,17 @@ import 'package:flame_forge2d/flame_forge2d.dart';

import 'boundaries.dart';

class ChopperBody extends PositionBodyComponent {
class ChopperBody extends BodyComponent {
final Vector2 position;
final Vector2 size;

ChopperBody(
this.position,
PositionComponent component,
) : super(positionComponent: component, size: component.size);
) : size = component.size {
renderBody = false;
add(component);
}

@override
Body createBody() {
Expand Down Expand Up @@ -64,6 +68,7 @@ class PositionBodySample extends Forge2DGame with TapDetector {
final animationComponent = SpriteAnimationComponent(
animation: animation,
size: spriteSize,
anchor: Anchor.center,
);
add(ChopperBody(position, animationComponent));
}
Expand Down
14 changes: 11 additions & 3 deletions packages/flame_forge2d/example/lib/sprite_body_sample.dart
Expand Up @@ -22,19 +22,27 @@ class SpriteBodySample extends Forge2DGame with TapDetector {
}
}

class Pizza extends PositionBodyComponent {
class Pizza extends BodyComponent {
final Vector2 position;
final Vector2 size;

Pizza(
this.position, {
Vector2? size,
}) : super(size: size ?? Vector2(2, 3));
}) : size = size ?? Vector2(2, 3);

@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite('pizza.png');
positionComponent = SpriteComponent(sprite: sprite, size: size);
renderBody = false;
add(
SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
),
);
}

@override
Expand Down
6 changes: 1 addition & 5 deletions packages/flame_forge2d/lib/body_component.dart
Expand Up @@ -7,8 +7,6 @@ import 'package:flutter/foundation.dart';
import 'package:forge2d/forge2d.dart' hide Timer, Vector2;

import 'forge2d_game.dart';
import 'position_body_component.dart';
import 'sprite_body_component.dart';

/// Since a pure BodyComponent doesn't have anything drawn on top of it,
/// it is a good idea to turn on [debugMode] for it so that the bodies can be
Expand All @@ -22,11 +20,9 @@ abstract class BodyComponent<T extends Forge2DGame> extends Component
/// the body wont be rendered. If you render something on top of the
/// [BodyComponent], or doesn't want it to be seen, you probably want to set
/// it to false.
/// [SpriteBodyComponent] and [PositionBodyComponent] has it set to false by
/// default.
bool renderBody = true;

BodyComponent({Paint? paint}) {
BodyComponent({Paint? paint, int? priority}) : super(priority: priority) {
this.paint = paint ?? (Paint()..color = defaultColor);
}

Expand Down
2 changes: 0 additions & 2 deletions packages/flame_forge2d/lib/flame_forge2d.dart
Expand Up @@ -6,5 +6,3 @@ export 'body_component.dart';
export 'contact_callbacks.dart';
export 'forge2d_camera.dart';
export 'forge2d_game.dart';
export 'position_body_component.dart';
export 'sprite_body_component.dart';
60 changes: 57 additions & 3 deletions packages/flame_forge2d/lib/forge2d_camera.dart
@@ -1,16 +1,28 @@
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';

import 'flame_forge2d.dart';

class Forge2DCamera extends Camera {
Body? _targetBody;

/// Used to minimize the amount of [Vector2] objects created
final Vector2 _unprojectVector = Vector2.zero();
final Vector2 _projectVector = Vector2.zero();

@override
Vector2 unprojectVector(Vector2 screenCoordinates) {
return position
..add(screenCoordinates / zoom)
_unprojectVector.setFrom(screenCoordinates);
return (_unprojectVector / zoom)
..add(position)
..y *= -1;
}

@override
Vector2 projectVector(Vector2 worldCoordinates) {
return ((worldCoordinates.clone()..y *= -1) - position)..scale(zoom);
_projectVector.setFrom(worldCoordinates);
return ((_projectVector..y *= -1) - position)..scale(zoom);
}

@override
Expand All @@ -22,4 +34,46 @@ class Forge2DCamera extends Camera {
Vector2 scaleVector(Vector2 worldCoordinates) {
return worldCoordinates * zoom;
}

@override
void update(double dt) {
if (_targetBody != null) {
if (follow == null) {
follow = _targetBody!.position.clone()..y *= -1;
} else {
follow!
..setFrom(_targetBody!.position)
..y *= -1;
}
}
super.update(dt);
}

/// Immediately snaps the camera to start following the [BodyComponent].
///
/// This means that the camera will move so that the position vector of the
/// component is in a fixed position on the screen.
/// That position is determined by a fraction of screen size defined by
/// [relativeOffset] (default to the center).
/// [worldBounds] can be optionally set to add boundaries to how far the
/// camera is allowed to move.
/// The component is "grabbed" by its anchor (default top left).
/// So for example if you want the center of the object to be at the fixed
/// position, set the components anchor to center.
void followBodyComponent(
BodyComponent bodyComponent, {
Anchor relativeOffset = Anchor.center,
Rect? worldBounds,
}) {
_targetBody = bodyComponent.body;
if (worldBounds != null) {
this.worldBounds = worldBounds;
}
setRelativeOffset(relativeOffset);
this.relativeOffset.setFrom(relativeOffset.toVector2());
}

void unfollowBodyComponent() {
_targetBody = null;
}
}
4 changes: 4 additions & 0 deletions packages/flame_forge2d/lib/forge2d_game.dart
Expand Up @@ -12,6 +12,10 @@ class Forge2DGame extends FlameGame {

final ContactCallbacks _contactCallbacks = ContactCallbacks();

/// The camera translates the coordinate space after the viewport is applied.
@override
Forge2DCamera get camera => super.camera as Forge2DCamera;

Forge2DGame({
Vector2? gravity,
double zoom = defaultZoom,
Expand Down
3 changes: 3 additions & 0 deletions packages/flame_forge2d/lib/position_body_component.dart
Expand Up @@ -9,6 +9,9 @@ import 'forge2d_game.dart';
/// [PositionComponent] and it can only have its anchor in the center.
/// You can initialize it either by sending in a [PositionComponent] and a
/// [size] in the constructor, or set those fields in [onLoad].
@Deprecated(
'Add children to BodyComponent instead. Will be removed in 0.10.0',
)
abstract class PositionBodyComponent<T extends Forge2DGame,
P extends PositionComponent> extends BodyComponent<T> {
P? positionComponent;
Expand Down
4 changes: 4 additions & 0 deletions packages/flame_forge2d/lib/sprite_body_component.dart
Expand Up @@ -3,6 +3,10 @@ import 'package:flame/components.dart';
import 'forge2d_game.dart';
import 'position_body_component.dart';

@Deprecated(
'Add a SpriteComponent as a child to the BodyComponent instead. '
'Will be removed in 0.10.0',
)
abstract class SpriteBodyComponent<T extends Forge2DGame>
extends PositionBodyComponent<T, SpriteComponent> {
/// Make sure that the [size] of the sprite matches the bounding shape of the
Expand Down

0 comments on commit 7fe8b6d

Please sign in to comment.