Skip to content

Commit

Permalink
fix: PropagatingCollisionBehavior should also work for non entity c…
Browse files Browse the repository at this point in the history
…omponents (#20)

* bug: `PropagatingCollisionBehavior` should also work for non entity components

* bug: `PropagatingCollisionBehavior` should also work for non entity components
  • Loading branch information
wolfenrain authored Jun 20, 2022
1 parent 64276fe commit ca5fc6c
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 108 deletions.
1 change: 1 addition & 0 deletions example/lib/behaviors/behaviors.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'movement_behavior.dart';
export 'rotation_behavior.dart';
export 'screen_collision_behavior.dart';
9 changes: 0 additions & 9 deletions example/lib/behaviors/rotation_behavior.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,5 @@ class RotationBehavior extends Behavior with HasGameRef {
void update(double dt) {
final angleDelta = dt * rotationSpeed;
parent.angle = (parent.angle + angleDelta) % (2 * pi);
// Takes rotation into consideration (which topLeftPosition doesn't)
final topLeft = parent.absoluteCenter - (parent.scaledSize / 2);
if (topLeft.x + parent.scaledSize.x < 0 ||
topLeft.y + parent.scaledSize.y < 0 ||
topLeft.x > screenHitbox.scaledSize.x ||
topLeft.y > screenHitbox.scaledSize.y) {
final moduloSize = screenHitbox.scaledSize + parent.scaledSize;
parent.topLeftPosition = parent.topLeftPosition % moduloSize;
}
}
}
21 changes: 21 additions & 0 deletions example/lib/behaviors/screen_collision_behavior.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flame/collisions.dart';
import 'package:flame_behaviors/flame_behaviors.dart';

/// Simplified "screen wrapping" behavior, while not perfect it does showcase
/// the possibility of acting on collision with non-entities.
class ScreenCollisionBehavior extends CollisionBehavior<ScreenHitbox, Entity> {
@override
void onCollisionEnd(ScreenHitbox other) {
if (parent.position.x < other.position.x) {
parent.position.x = other.position.x + other.scaledSize.x;
} else if (parent.position.x > other.position.x + other.scaledSize.x) {
parent.position.x = other.position.x;
}

if (parent.position.y < other.position.y) {
parent.position.y = other.position.y + other.scaledSize.y;
} else if (parent.position.y > other.position.y + other.scaledSize.y) {
parent.position.y = other.position.y;
}
}
}
1 change: 1 addition & 0 deletions example/lib/entities/circle/circle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Circle extends Entity with HasPaint {
PropagatingCollisionBehavior(CircleHitbox()),
CircleCollisionBehavior(),
RectangleCollisionBehavior(),
ScreenCollisionBehavior(),
MovementBehavior(velocity: velocity),
RotationBehavior(rotationSpeed: rotationSpeed),
DraggingBehavior()
Expand Down
1 change: 1 addition & 0 deletions example/lib/entities/rectangle/rectangle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Rectangle extends Entity with HasPaint {
PropagatingCollisionBehavior(RectangleHitbox()),
RectangleCollisionBehavior(),
CircleCollisionBehavior(),
ScreenCollisionBehavior(),
MovementBehavior(velocity: velocity),
RotationBehavior(rotationSpeed: rotationSpeed),
FreezeBehavior(),
Expand Down
11 changes: 2 additions & 9 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ExampleGame extends FlameGame
await add(ScreenHitbox());

var shapeEntity = _randomEntity(
position: Vector2.zero(),
position: size / 2,
size: Vector2.all(50) + Vector2.random(_rng) * 100,
);
await add(shapeEntity);
Expand All @@ -26,17 +26,10 @@ class ExampleGame extends FlameGame
}

final _rng = Random();
final _distance = Vector2(100, 0);

Entity nextRandomEntity(Entity entity) {
final size = Vector2.all(50) + Vector2.random(_rng) * 100;
final isXOverflow =
entity.position.x + entity.size.x / 2 + _distance.x + size.x > size.x;
var position = _distance + Vector2(0, entity.position.y + 200);
if (!isXOverflow) {
position = (entity.position + _distance)..x += size.x / 2;
}
return _randomEntity(position: position, size: size);
return _randomEntity(position: this.size / 2, size: size);
}

Entity _randomEntity({
Expand Down
26 changes: 16 additions & 10 deletions lib/src/behaviors/propagating_collision_behavior.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ abstract class CollisionBehavior<Collider extends Component,
bool get isColliding {
final propagatingCollisionBehavior =
parent.findBehavior<PropagatingCollisionBehavior>();
final activeCollisions = propagatingCollisionBehavior.activeCollisions;

return activeCollisions
return propagatingCollisionBehavior.activeCollisions
.map(propagatingCollisionBehavior._findEntity)
.whereType<Collider>()
.isNotEmpty;
Expand All @@ -54,9 +53,9 @@ abstract class CollisionBehavior<Collider extends Component,
/// Any other entity that has a [CollisionBehavior] for that entity attached
/// will then be able to collide with it.
///
/// **Note**: This behavior can only be used for collisions between entities.
/// It cannot be used for collisions between an entity and a non-entity
/// component.
/// **Note**: This behavior can also be used for collisions between entities
/// and non-entity components, by passing the component's type as the
/// `Collider` to the [CollisionBehavior].
/// {@endtemplate}
class PropagatingCollisionBehavior extends Behavior with CollisionCallbacks {
/// {@macro propagating_collision_behavior}
Expand All @@ -77,10 +76,17 @@ class PropagatingCollisionBehavior extends Behavior with CollisionCallbacks {
/// List of [CollisionBehavior]s to which it can propagate to.
List<CollisionBehavior> _propagateToBehaviors = [];

Entity? _findEntity(PositionComponent other) {
/// Tries to find the entity that is colliding with the given entity.
///
/// It will check if the parent is either a [PropagatingCollisionBehavior]
/// or a [Entity]. If it is neither, it will return [other].
Component? _findEntity(PositionComponent other) {
final parent = other.parent;
if (parent is! PropagatingCollisionBehavior && parent is! Entity) {
return null;
if (other is ShapeHitbox) {
return other.parent;
}
return other;
}

return parent is Entity
Expand All @@ -96,7 +102,7 @@ class PropagatingCollisionBehavior extends Behavior with CollisionCallbacks {
activeCollisions.add(other);
final otherEntity = _findEntity(other);
if (otherEntity == null) {
return super.onCollisionStart(intersectionPoints, other);
return;
}

for (final behavior in _propagateToBehaviors) {
Expand All @@ -112,7 +118,7 @@ class PropagatingCollisionBehavior extends Behavior with CollisionCallbacks {
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
final otherEntity = _findEntity(other);
if (otherEntity == null) {
return super.onCollision(intersectionPoints, other);
return;
}

for (final behavior in _propagateToBehaviors) {
Expand All @@ -128,7 +134,7 @@ class PropagatingCollisionBehavior extends Behavior with CollisionCallbacks {
activeCollisions.remove(other);
final otherEntity = _findEntity(other);
if (otherEntity == null) {
return super.onCollisionEnd(other);
return;
}

for (final behavior in _propagateToBehaviors) {
Expand Down
114 changes: 34 additions & 80 deletions test/src/behaviors/propagating_collision_behavior_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ class _EntityB extends Entity {
}

class _EntityC extends Entity {
_EntityC({
super.children,
}) : super(size: Vector2.all(16));
_EntityC() : super(size: Vector2.all(16));
}

abstract class _CollisionBehavior<A extends Entity, B extends Entity>
abstract class _CollisionBehavior<A extends Component, B extends Entity>
extends CollisionBehavior<A, B> {
bool onCollisionStartCalled = false;
bool onCollisionCalled = false;
Expand Down Expand Up @@ -55,6 +53,9 @@ class _CollisionBehaviorAtoB extends _CollisionBehavior<_EntityB, _EntityA> {}

class _CollisionBehaviorAtoC extends _CollisionBehavior<_EntityC, _EntityA> {}

class _CollisionBehaviorAtoComponent
extends _CollisionBehavior<PositionComponent, _EntityA> {}

void main() {
final flameTester = FlameTester(TestGame.new);

Expand Down Expand Up @@ -134,25 +135,37 @@ void main() {
(game) async {
final collisionBehaviorAtoB = _CollisionBehaviorAtoB();
final collisionBehaviorAtoC = _CollisionBehaviorAtoC();
final collisionBehaviorAtoComponent =
_CollisionBehaviorAtoComponent();
final entityA = _EntityA(
behaviors: [
PropagatingCollisionBehavior(RectangleHitbox()),
collisionBehaviorAtoB,
collisionBehaviorAtoC
collisionBehaviorAtoC,
collisionBehaviorAtoComponent,
],
);

final entityB = _EntityB(
behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],
);

final positionComponent = PositionComponent(
size: Vector2.all(16),
children: [
RectangleHitbox(),
],
);

await game.ensureAdd(entityA);
await game.ensureAdd(entityB);
await game.ensureAdd(positionComponent);

game.update(0);

expect(collisionBehaviorAtoB.onCollisionStartCalled, isTrue);
expect(collisionBehaviorAtoC.onCollisionStartCalled, isFalse);
expect(collisionBehaviorAtoComponent.onCollisionStartCalled, isTrue);
},
);

Expand Down Expand Up @@ -184,86 +197,22 @@ void main() {
'on end to the correct collision behavior',
(game) async {
final collisionBehaviorAtoB = _CollisionBehaviorAtoB();
final collisionBehaviorAtoC = _CollisionBehaviorAtoC();
final collisionBehaviorAtoComponent =
_CollisionBehaviorAtoComponent();
final entityA = _EntityA(
behaviors: [
PropagatingCollisionBehavior(RectangleHitbox()),
collisionBehaviorAtoB,
collisionBehaviorAtoC,
collisionBehaviorAtoComponent,
],
);

final entityB = _EntityB(
behaviors: [PropagatingCollisionBehavior(RectangleHitbox())],
);

await game.ensureAdd(entityA);
await game.ensureAdd(entityB);

game.update(0);

expect(collisionBehaviorAtoB.onCollisionEndCalled, isFalse);

entityB.position += Vector2.all(50);

game.update(0);

expect(collisionBehaviorAtoB.onCollisionEndCalled, isTrue);
},
);

group('only if it collides with an entity', () {
flameTester.test('on start', (game) async {
final collisionBehaviorAtoB = _CollisionBehaviorAtoB();
final collisionBehaviorAtoC = _CollisionBehaviorAtoC();
final entityA = _EntityA(
behaviors: [
PropagatingCollisionBehavior(RectangleHitbox()),
collisionBehaviorAtoB,
collisionBehaviorAtoC
],
);

await game.ensureAdd(entityA);
await game.ensureAdd(
PositionComponent(
size: Vector2.all(16),
children: [
RectangleHitbox(),
],
),
);
await game.ensureAdd(
_EntityC(
children: [
RectangleHitbox(),
],
),
);

game.update(0);

expect(collisionBehaviorAtoB.onCollisionStartCalled, isFalse);
expect(collisionBehaviorAtoC.onCollisionStartCalled, isTrue);

expect(collisionBehaviorAtoB.onCollisionCalled, isFalse);
expect(collisionBehaviorAtoC.onCollisionCalled, isTrue);
});
flameTester.test('on end', (game) async {
final collisionBehaviorAtoB = _CollisionBehaviorAtoB();
final collisionBehaviorAtoC = _CollisionBehaviorAtoC();
final entityA = _EntityA(
behaviors: [
PropagatingCollisionBehavior(RectangleHitbox()),
collisionBehaviorAtoB,
collisionBehaviorAtoC
],
);

final entityC = _EntityC(
children: [
RectangleHitbox(),
],
);

final positionComponent = PositionComponent(
size: Vector2.all(16),
children: [
Expand All @@ -272,20 +221,25 @@ void main() {
);

await game.ensureAdd(entityA);
await game.ensureAdd(entityB);
await game.ensureAdd(positionComponent);
await game.ensureAdd(entityC);

game.update(0);

entityC.position += Vector2.all(50);
expect(collisionBehaviorAtoB.onCollisionEndCalled, isFalse);
expect(collisionBehaviorAtoC.onCollisionEndCalled, isFalse);
expect(collisionBehaviorAtoComponent.onCollisionEndCalled, isFalse);

entityB.position += Vector2.all(50);
positionComponent.position += Vector2.all(50);

game.update(0);

expect(collisionBehaviorAtoB.onCollisionEndCalled, isFalse);
expect(collisionBehaviorAtoC.onCollisionEndCalled, isTrue);
});
});
expect(collisionBehaviorAtoB.onCollisionEndCalled, isTrue);
expect(collisionBehaviorAtoC.onCollisionEndCalled, isFalse);
expect(collisionBehaviorAtoComponent.onCollisionEndCalled, isTrue);
},
);
});
});
}

0 comments on commit ca5fc6c

Please sign in to comment.