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!: Move Forge2DGame to use CameraComponent #2728

Merged
merged 33 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e43cb21
Rename example
spydon Jun 7, 2023
6520253
flame_forge2d examples wip
spydon Jun 11, 2023
56e5374
Start converting forge2d examples
spydon Jul 16, 2023
6e6c3cd
Events now work in Forge2D
spydon Jul 19, 2023
4d9c78a
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Jul 19, 2023
5e76cc5
Fix all non-joint examples
spydon Jul 19, 2023
1a1cf5f
WIP fixing joint examples
spydon Jul 20, 2023
fccc65e
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Jul 24, 2023
726a278
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Aug 18, 2023
918638e
Fix analyze
spydon Aug 24, 2023
53dd6e2
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Sep 10, 2023
84fe542
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Sep 11, 2023
0b63e5a
Avoid creating new Vector2 in globalToLocal and localToGlobal
spydon Sep 11, 2023
2070a7e
Fix examples
spydon Sep 11, 2023
b220b71
Fix exports
spydon Sep 11, 2023
38b1944
Fix examples name
spydon Sep 11, 2023
2409bd5
Update docs
spydon Sep 11, 2023
2818512
Fix analyze
spydon Sep 11, 2023
c23790f
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 11, 2023
d5a74f3
Fix tests
spydon Sep 11, 2023
0a68dc1
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 12, 2023
7a957ee
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 12, 2023
0da0eb9
Update doc/bridge_packages/flame_forge2d/forge2d.md
spydon Sep 12, 2023
112e1a6
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 12, 2023
eada637
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 12, 2023
7783ab0
Fix up examples
spydon Sep 13, 2023
248efea
Fix padracing
spydon Sep 13, 2023
475129f
Fix comment
spydon Sep 14, 2023
4c26d64
Merge remote-tracking branch 'origin/main' into spydon/move-forge2d-t…
spydon Sep 14, 2023
814d980
The assertion message is not important
spydon Sep 14, 2023
299ad75
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 15, 2023
23cf212
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 15, 2023
e9de3cd
Merge branch 'main' into spydon/move-forge2d-to-cameracomponent
spydon Sep 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
52 changes: 24 additions & 28 deletions doc/bridge_packages/flame_forge2d/forge2d.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Forge2D

We (the Flame organization) maintain a ported version of the Box2D physics engine and our version
is called Forge2D.
Blue Fire maintains a ported version of the Box2D physics engine and our
version is called Forge2D.

If you want to use Forge2D specifically for Flame you should use our bridge library
[flame_forge2d](https://github.com/flame-engine/flame/tree/main/packages/flame_forge2d) and if you
Expand All @@ -20,23 +20,24 @@ instructions](https://pub.dev/packages/flame_forge2d)](<https://pub.dev/packages
If you are going to use Forge2D in your project it can be a good idea to use the Forge2D-specific
`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.
It is called `Forge2DGame` and supports both the special Forge2D components called `BodyComponents`
as well as normal Flame components.

In `Forge2DGame` the `Camera` has a zoom level set to 10 by default, so your components will be a
lot bigger than in a normal Flame game. This is due to the speed limit in the `Forge2D` world, which
you would hit very quickly if you are using it with `zoom = 1.0`. You can easily change the zoom
level either by calling `super(zoom: yourZoom)` in your constructor or doing `game.camera.zoom =
yourZoom;` at a later stage.
`Forge2DGame` has a built-in `CameraComponent` and has a zoom level set to 10 by default, so your
components will be a lot bigger than in a normal Flame game. This is due to the speed limit in the
`Forge2D` world, which you would hit very quickly if you are using it with `zoom = 1.0`. You can
easily change the zoom level either by calling `super(zoom: yourZoom)` in your constructor or
doing `game.cameraComponent.viewfinder.zoom = yourZoom;` at a later stage.

If you are previously familiar with Box2D it can be good to know that the whole concept of the
Box2d world is mapped to `world` in the `Forge2DGame` component and every `Body` that you want to
use as a component should be wrapped in a `BodyComponent`, and added to your `Forge2DGame`.
use as a component should be wrapped in a `BodyComponent`, and added to the `world` in your
`Forge2DGame`.

You can have for example a HUD and other non-physics-related components in your `Forge2DGame`'s
component list along with your physical entities. When the update is called, it will use the Forge2D
physics engine to properly update every `BodyComponent` and other components in the game will be
updated according to the normal `FlameGame` way.
You can have have non-physics-related components in your `Forge2DGame` world's component list along
with your physical entities. When the update is called, it will use the Forge2D physics engine to
properly update every `BodyComponent` and other components in the game will be updated according to
the normal `FlameGame` way.

In `Forge2DGame` the gravity is flipped compared to `Forge2D` to keep the same coordinate system as
in Flame, so a positive y-axis in the gravity like `Vector2(0, 10)` would be pulling bodies
Expand Down Expand Up @@ -65,27 +66,30 @@ not according to the coordinate system of Forge2D (where the Y-axis is flipped).

:exclamation: In Forge2D you shouldn't add any bodies as children to other components,
since Forge2D doesn't have a concept of nested bodies.
So bodies should live on the top level in the physics world.
Instead of `add(Bullet()))`, `parent?.add(Bullet())` should be used (as bellow).
So bodies should live on the top level in the physics world, `Forge2DGame.world`.
So instead of `add(Weapon()))`, `world.add(Weapon())` should be used (as below), and the `Player`
should also of course initially be added to the world.

```dart
class Bullet extends BodyComponent {
class Weapon extends BodyComponent {
@override
void onLoad() {
...
isBullet = true;
...
}
}

class Player extends BodyComponent {
@override
void onLoad() {
parent?.add(Bullet());
world.add(Weapon());
}
}
```

Later you might want to add bullets coming from your weapon, these are added to the world in the
same sense, but if they are going to be moving very fast, make sure that you set `isBullet = true`
to avoid some tunneling problems.


## Contact callbacks

Expand Down Expand Up @@ -139,11 +143,3 @@ fixtures cease being in contact, `endContact` will be called.

An implementation example can be seen in the [Flame Forge2D
example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/bridge_libraries/forge2d/utils/balls.dart).


### 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#camerafollowcomponent). When you want to
stop following a `BodyComponent` you should call `camera.unfollowBodyComponent`.
15 changes: 8 additions & 7 deletions examples/games/padracing/lib/ball.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import 'package:padracing/wall.dart';

class Ball extends BodyComponent<PadRacingGame> with ContactCallbacks {
final double radius;
final Vector2 position;
final Vector2 initialPosition;
final double rotation;
final bool isMovable;
final rng = Random();
late final Paint _shaderPaint;

Ball({
required this.position,
required this.initialPosition,
this.radius = 80.0,
this.rotation = 1.0,
this.isMovable = true,
Expand Down Expand Up @@ -48,7 +48,7 @@ class Ball extends BodyComponent<PadRacingGame> with ContactCallbacks {
final def = BodyDef()
..userData = this
..type = isMovable ? BodyType.dynamic : BodyType.kinematic
..position = position;
..position = initialPosition;
final body = world.createBody(def)..angularVelocity = rotation;

final shape = CircleShape()..radius = radius;
Expand All @@ -72,7 +72,7 @@ class Ball extends BodyComponent<PadRacingGame> with ContactCallbacks {
}

late Rect asRect = Rect.fromCircle(
center: position.toOffset(),
center: initialPosition.toOffset(),
radius: radius,
);
}
Expand All @@ -82,12 +82,13 @@ List<Ball> createBalls(Vector2 trackSize, List<Wall> walls, Ball bigBall) {
final rng = Random();
while (balls.length < 20) {
final ball = Ball(
position: Vector2.random(rng)..multiply(trackSize),
initialPosition: Vector2.random(rng)..multiply(trackSize),
radius: 3.0 + rng.nextInt(5),
rotation: (rng.nextBool() ? 1 : -1) * rng.nextInt(5).toDouble(),
);
final touchesBall = ball.position.distanceTo(bigBall.position) <
ball.radius + bigBall.radius;
final touchesBall =
ball.initialPosition.distanceTo(bigBall.initialPosition) <
ball.radius + bigBall.radius;
if (!touchesBall) {
final touchesWall =
walls.any((wall) => wall.asRect.overlaps(ball.asRect));
Expand Down
4 changes: 2 additions & 2 deletions examples/games/padracing/lib/car.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ class Car extends BodyComponent<PadRacingGame> {
final isLeftTire = i.isEven;
return Tire(
car: this,
pressedKeys: gameRef.pressedKeySets[playerNumber],
pressedKeys: game.pressedKeySets[playerNumber],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the rename from gameRef to game?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is from the really old Pasha PR where we are moving from the HasGameRef mixin to HasGameReference.

isFrontTire: isFrontTire,
isLeftTire: isLeftTire,
jointDef: jointDef,
isTurnableTire: isFrontTire,
);
});

gameRef.cameraWorld.addAll(tires);
game.world.addAll(tires);
return body;
}

Expand Down
6 changes: 3 additions & 3 deletions examples/games/padracing/lib/lap_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import 'package:padracing/car.dart';
import 'package:padracing/game_colors.dart';

class LapLine extends BodyComponent with ContactCallbacks {
LapLine(this.id, this.position, this.size, {required this.isFinish})
LapLine(this.id, this.initialPosition, this.size, {required this.isFinish})
: super(priority: 1);

final int id;
final bool isFinish;
final Vector2 position;
final Vector2 initialPosition;
final Vector2 size;
late final Rect rect = size.toRect();
Image? _finishOverlay;
Expand Down Expand Up @@ -45,7 +45,7 @@ class LapLine extends BodyComponent with ContactCallbacks {

final groundBody = world.createBody(
BodyDef(
position: position,
position: initialPosition,
userData: this,
),
);
Expand Down
7 changes: 4 additions & 3 deletions examples/games/padracing/lib/lap_text.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flutter/material.dart' hide Image, Gradient;
import 'package:google_fonts/google_fonts.dart';

import 'package:padracing/car.dart';
import 'package:padracing/padracing_game.dart';

class LapText extends PositionComponent with HasGameRef<PadRacingGame> {
class LapText extends PositionComponent with HasGameReference<PadRacingGame> {
LapText({required this.car, required Vector2 position})
: super(position: position);

Expand Down Expand Up @@ -66,10 +67,10 @@ class LapText extends PositionComponent with HasGameRef<PadRacingGame> {

@override
void update(double dt) {
if (gameRef.isGameOver) {
if (game.isGameOver) {
return;
}
_timePassedComponent.text = gameRef.timePassed;
_timePassedComponent.text = game.timePassed;
}

final _backgroundRect = RRect.fromRectAndRadius(
Expand Down
19 changes: 8 additions & 11 deletions examples/games/padracing/lib/padracing_game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
static final Vector2 trackSize = Vector2.all(500);
static const double playZoom = 8.0;
static const int numberOfLaps = 3;
late final World cameraWorld;
late CameraComponent startCamera;
late List<Map<LogicalKeyboardKey, LogicalKeyboardKey>> activeKeyMaps;
late List<Set<LogicalKeyboardKey>> pressedKeySets;
Expand All @@ -58,13 +57,13 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {

@override
Future<void> onLoad() async {
super.onLoad();
cameraComponent.removeFromParent();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this is because it gets re-added later and you want to call onLoad to reset the game? just curious what breaks if you just leave the camera there as is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This camera isn't used, it uses several other cameras later instead.

children.register<CameraComponent>();
cameraWorld = World();
add(cameraWorld);

final walls = createWalls(trackSize);
final bigBall = Ball(position: Vector2(200, 245), isMovable: false);
cameraWorld.addAll([
final bigBall = Ball(initialPosition: Vector2(200, 245), isMovable: false);
world.addAll([
LapLine(1, Vector2(25, 50), Vector2(50, 5), isFinish: false),
LapLine(2, Vector2(25, 70), Vector2(50, 5), isFinish: false),
LapLine(3, Vector2(52.5, 25), Vector2(5, 50), isFinish: true),
Expand All @@ -82,9 +81,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
canvasSize.x / trackSize.x,
canvasSize.y / trackSize.y,
);
startCamera = CameraComponent(
world: cameraWorld,
)
startCamera = CameraComponent(world: world)
..viewfinder.position = trackSize / 2
..viewfinder.anchor = Anchor.center
..viewfinder.zoom = zoomLevel - 0.2;
Expand Down Expand Up @@ -136,7 +133,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
..paint.style = PaintingStyle.stroke;
final cameras = List.generate(numberOfPlayers, (i) {
return CameraComponent(
world: cameraWorld,
world: world,
viewport: FixedSizeViewport(viewportSize.x, viewportSize.y)
..position = alignedVector(
longMultiplier: i == 0 ? 0.0 : 1 / (i + 1),
Expand All @@ -152,7 +149,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
const mapCameraZoom = 0.5;
final mapCameras = List.generate(numberOfPlayers, (i) {
return CameraComponent(
world: cameraWorld,
world: world,
viewport: FixedSizeViewport(mapCameraSize.x, mapCameraSize.y)
..position = Vector2(
viewportSize.x - mapCameraSize.x * mapCameraZoom - 50,
Expand Down Expand Up @@ -193,7 +190,7 @@ class PadRacingGame extends Forge2DGame with KeyboardEvents {
}
});
cars.add(car);
cameraWorld.add(car);
world.add(car);
cameras[i].viewport.addAll([lapText, mapCameras[i]]);
}

Expand Down
4 changes: 2 additions & 2 deletions examples/games/padracing/lib/tire.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class Tire extends BodyComponent<PadRacingGame> {
@override
Future<void> onLoad() async {
await super.onLoad();
gameRef.cameraWorld.add(Trail(car: car, tire: this));
game.world.add(Trail(car: car, tire: this));
}

@override
Expand Down Expand Up @@ -94,7 +94,7 @@ class Tire extends BodyComponent<PadRacingGame> {
if (body.isAwake || pressedKeys.isNotEmpty) {
_updateTurn(dt);
_updateFriction();
if (!gameRef.isGameOver) {
if (!game.isGameOver) {
_updateDrive();
}
}
Expand Down
8 changes: 4 additions & 4 deletions examples/games/padracing/lib/wall.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ List<Wall> createWalls(Vector2 size) {
}

class Wall extends BodyComponent<PadRacingGame> {
Wall(this.position, this.size) : super(priority: 3);
Wall(this._position, this.size) : super(priority: 3);

final Vector2 position;
final Vector2 _position;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine things like this and initialPosition are to avoid conflict with a position defined on BodyComponent.
but why can't we just use that position for everything? where do components need to keep a separate track of a second position?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because position is a read-only on BodyComponent since it is a getter from the Body.

final Vector2 size;

final Random rng = Random();
Expand Down Expand Up @@ -94,7 +94,7 @@ class Wall extends BodyComponent<PadRacingGame> {
Body createBody() {
final def = BodyDef()
..type = BodyType.static
..position = position;
..position = _position;
final body = world.createBody(def)
..userData = this
..angularDamping = 3.0;
Expand All @@ -105,7 +105,7 @@ class Wall extends BodyComponent<PadRacingGame> {
}

late Rect asRect = Rect.fromCenter(
center: position.toOffset(),
center: _position.toOffset(),
width: size.x,
height: size.y,
);
Expand Down
24 changes: 12 additions & 12 deletions examples/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import 'package:examples/platform/stub_provider.dart'
if (dart.library.html) 'platform/web_provider.dart';
import 'package:examples/stories/animations/animations.dart';
import 'package:examples/stories/bridge_libraries/audio/audio.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart';
import 'package:examples/stories/bridge_libraries/flame_isolate/isolate.dart';
import 'package:examples/stories/bridge_libraries/flame_lottie/lottie.dart';
import 'package:examples/stories/bridge_libraries/flame_spine/flame_spine.dart';
import 'package:examples/stories/bridge_libraries/forge2d/flame_forge2d.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/constant_volume_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/distance_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/friction_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/gear_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/motor_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/mouse_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/prismatic_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/pulley_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/revolute_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/rope_joint.dart';
import 'package:examples/stories/bridge_libraries/forge2d/joints/weld_joint.dart';
import 'package:examples/stories/camera_and_viewport/camera_and_viewport.dart';
import 'package:examples/stories/collision_detection/collision_detection.dart';
import 'package:examples/stories/components/components.dart';
Expand Down