Skip to content

Commit

Permalink
fix: Absolute angle takes into account BodyComponent ancestors too (#…
Browse files Browse the repository at this point in the history
…2678)

Currently, the implementation of the `absoluteAngle` getter in
`PositionComponent` only takes into account `PositionComponent`
ancestors. The implementation simply runs through the whole hierarchy
summing up the relative angles.

This gives a wrong result with Forge2D because `BodyComponent`s have
angles too: in the common situation where a `PositionComponent` (e.g. a
`SpriteComponent`) is the child of a `BodyComponent`, the
`BodyComponent`'s rotation is ignored when computing the
`absoluteAngle`.

The fix simply introduces a new minimal interface `HasAngle` that both
`PositionComponent` and `BodyComponent` implement (`PositionComponent`
implements it automatically via `AngleProvider`), and reimplements
`absoluteAngle` so that it considers all ancestors with an angle and not
just the `PositionComponent` ones.
  • Loading branch information
maurovanetti committed Aug 25, 2023
1 parent b77802a commit 75aee76
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 13 deletions.
2 changes: 2 additions & 0 deletions packages/flame/lib/effects.dart
Expand Up @@ -30,9 +30,11 @@ export 'src/effects/provider_interfaces.dart'
show
AnchorProvider,
AngleProvider,
ReadOnlyAngleProvider,
PositionProvider,
ScaleProvider,
SizeProvider,
ReadOnlySizeProvider,
OpacityProvider;
export 'src/effects/remove_effect.dart';
export 'src/effects/rotate_effect.dart';
Expand Down
4 changes: 2 additions & 2 deletions packages/flame/lib/src/components/core/component.dart
Expand Up @@ -826,8 +826,8 @@ class Component {
assert(isLoaded && !isLoading);
_setMountingBit();
onGameResize(_parent!.findGame()!.canvasSize);
if (_parent is ReadonlySizeProvider) {
onParentResize((_parent! as ReadonlySizeProvider).size);
if (_parent is ReadOnlySizeProvider) {
onParentResize((_parent! as ReadOnlySizeProvider).size);
}
if (isRemoved) {
_clearRemovedBit();
Expand Down
Expand Up @@ -35,15 +35,15 @@ class HudMarginComponent extends PositionComponent {
/// from the edges of the viewport can be used instead.
EdgeInsets? margin;

late ReadonlySizeProvider? _sizeProvider;
late ReadOnlySizeProvider? _sizeProvider;

@override
@mustCallSuper
void onMount() {
super.onMount();
_sizeProvider =
ancestors().firstWhereOrNull((c) => c is ReadonlySizeProvider)
as ReadonlySizeProvider?;
ancestors().firstWhereOrNull((c) => c is ReadOnlySizeProvider)
as ReadOnlySizeProvider?;
assert(
_sizeProvider != null,
'The parent of a HudMarginComponent needs to provide a size, for example '
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/components/position_component.dart
Expand Up @@ -231,7 +231,7 @@ class PositionComponent extends Component
double get absoluteAngle {
// TODO(spydon): take scale into consideration
return ancestors(includeSelf: true)
.whereType<PositionComponent>()
.whereType<ReadOnlyAngleProvider>()
.map((c) => c.angle)
.sum;
}
Expand Down
13 changes: 9 additions & 4 deletions packages/flame/lib/src/effects/provider_interfaces.dart
Expand Up @@ -34,9 +34,14 @@ abstract class ScaleProvider {
set scale(Vector2 value);
}

/// Interface for a component that can be affected by rotation effects.
abstract class AngleProvider {
/// Interface for a class that has [angle] property which can be read but not
/// modified.
abstract class ReadOnlyAngleProvider {
double get angle;
}

/// Interface for a component that can be affected by rotation effects.
abstract class AngleProvider extends ReadOnlyAngleProvider {
set angle(double value);
}

Expand All @@ -48,12 +53,12 @@ abstract class AnchorProvider {

/// Interface for a class that has [size] property which can be read but not
/// modified.
abstract class ReadonlySizeProvider {
abstract class ReadOnlySizeProvider {
Vector2 get size;
}

/// Interface for a component that can be affected by size effects.
abstract class SizeProvider extends ReadonlySizeProvider {
abstract class SizeProvider extends ReadOnlySizeProvider {
set size(Vector2 value);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/game/flame_game.dart
Expand Up @@ -20,7 +20,7 @@ import 'package:meta/meta.dart';
/// It is based on the Flame Component System (also known as FCS).
class FlameGame extends ComponentTreeRoot
with Game
implements ReadonlySizeProvider {
implements ReadOnlySizeProvider {
FlameGame({
super.children,
Camera? camera,
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/layout/align_component.dart
Expand Up @@ -108,7 +108,7 @@ class AlignComponent extends PositionComponent {
@override
void onMount() {
assert(
parent is ReadonlySizeProvider,
parent is ReadOnlySizeProvider,
"An AlignComponent's parent must have a size",
);
}
Expand Down
8 changes: 7 additions & 1 deletion packages/flame_forge2d/lib/body_component.dart
@@ -1,6 +1,7 @@
import 'dart:ui';

import 'package:flame/components.dart' hide World;
import 'package:flame/effects.dart' show ReadOnlyAngleProvider;
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/forge2d_game.dart';
Expand All @@ -11,7 +12,8 @@ import 'package:forge2d/forge2d.dart' hide Timer, Vector2;
/// it is a good idea to turn on [debugMode] for it so that the bodies can be
/// seen
abstract class BodyComponent<T extends Forge2DGame> extends Component
with HasGameRef<T>, HasPaint {
with HasGameRef<T>, HasPaint
implements ReadOnlyAngleProvider {
BodyComponent({
Paint? paint,
super.children,
Expand Down Expand Up @@ -46,10 +48,14 @@ abstract class BodyComponent<T extends Forge2DGame> extends Component
}

World get world => gameRef.world;

// TODO(Lukas): Use CameraComponent here instead.
// ignore: deprecated_member_use
Camera get camera => gameRef.camera;

Vector2 get center => body.worldCenter;

@override
double get angle => body.angle;

/// The matrix used for preparing the canvas
Expand Down
39 changes: 39 additions & 0 deletions packages/flame_forge2d/test/body_component_test.dart
@@ -1,5 +1,6 @@
// ignore_for_file: deprecated_member_use

import 'package:flame/components.dart' show PositionComponent;
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
Expand Down Expand Up @@ -325,5 +326,43 @@ void main() {
).called(1);
});
});

group('PositionComponent parented by BodyComponent', () {
final flameTester = FlameTester(Forge2DGame.new);

flameTester.testGameWidget(
'absoluteAngle',
setUp: (game, tester) async {
// Creates a body with an angle of 2 radians
final body = game.world.createBody(BodyDef(angle: 2.0));
final shape = EdgeShape()
..set(
Vector2.zero(),
Vector2.all(10),
);
body.createFixture(FixtureDef(shape));
final bodyComponent = _TestBodyComponent()..body = body;

// Creates a positional component with an angle of 1 radians
final positionComponent = PositionComponent(angle: 1.0);

// Creates a hierarchy: game > bodyComponent > positionComponent
bodyComponent.addToParent(game);
positionComponent.addToParent(bodyComponent);

await game.ready();

// Checks the hierarchy
expect(game.contains(bodyComponent), true);
expect(bodyComponent.contains(positionComponent), true);
expect(game.children.length, 1);
expect(bodyComponent.children.length, 1);
expect(positionComponent.children.length, 0);

// Expects the absolute angle to be (2 + 1) radians
expect(positionComponent.absoluteAngle, 3.0);
},
);
});
});
}

0 comments on commit 75aee76

Please sign in to comment.