Skip to content

Commit

Permalink
feat!: enhance ContactCallback process (#1547)
Browse files Browse the repository at this point in the history
Migration notes

The entire contact callback process has been redefined to mimic Flame's CollisionCallbacks.

In order to simplify the migration to this new code, the following steps should facilitate doing so:

    Remove lines of code that used addContactCallback, removeContactCallback, clearContactCallback.
    Remove subclasses of ContactCallbacks<Type1, Type2>.
    2.1. Include with ContactCallback in your BodyComponent class signature.
    2.2. Override the contact method that you are interested in, and check that other is Type2, then perform the logic.

The above is just a suggested migration approach. There are other code alternatives or patterns you might want to follow. In addition, you can define your own custom contact logic by providing a ContactListener to your Forge2DGame.

For more information check the examples, Flame documentation, or the class documentation of ContactCallbacks and WorldContactListener.
  • Loading branch information
alestiago committed Apr 22, 2022
1 parent 71999b1 commit a50d4a1
Show file tree
Hide file tree
Showing 11 changed files with 610 additions and 162 deletions.
69 changes: 34 additions & 35 deletions doc/other_modules/forge2d.md
Expand Up @@ -65,57 +65,56 @@ not according to the coordinate system of Forge2D (where the Y-axis is flipped).

## Contact callbacks

If you are using `Forge2DGame` you can take advantage of its way of handling contacts between two
`BodyComponent`s.
`Forge2DGame` provides a simple out of the box solution to propagate contact events.

When creating the body definition for your `BodyComponent` make sure that you set the userdata to
the current object, otherwise it will not be possible to detect collisions.
Like this:
```dart
final bodyDef = BodyDef()
// To be able to know which component that is involved in a collision
..userData = this;
```
Contact events occur whenever two `Fixture`s meet each other. These events allows listening when
these `Fixture`s begin to come in contact (`beginContact`) and cease being in contact
(`endContact`).

Now you have to make an implementation of `ContactCallback` where you set which two types that it
should react when they come in contact.
If you have two `BodyComponent`s named `Ball` and `Wall` and you want to do something when they come
in contact, you could do something like this:
There are multiple ways to listen to these events. One common way is to use the `ContactCallbacks`
class as a mixin in the `BodyComponent` where you are interested in these events.

```dart
class BallWallCallback extends ContactCallback<Ball, Wall> {
BallWallCallback();
@override
void begin(Ball ball, Wall wall, Contact contact) {
wall.remove();
class Ball extends BodyComponent with ContactCallbacks {
...
void beginContact(Object other, Contact contact) {
if (other is Wall) {
// Do something here.
}
}
@override
void end(Ball ball, Wall wall, Contact contact) {}
...
}
```

and then you simply add `BallWallCallback` to your `Forge2DGame`:
In order for the above to work, the `Ball`'s `body.userData` or contacting `fixture.userData` must be
set to a `ContactCallback`. And if `Wall` is a `BodyComponent` its `body.userData` or contacting
`fixture.userData` must be set to `Wall`.

If `userData` is `null` the contact events are ignored, it is `null` by default.

A convenient way of setting `userData` is to assign it when creating the body. For example:

```dart
class MyGame extends Forge2DGame {
MyGame(Forge2DComponent box) : super(box) {
addContactCallback(BallWallCallback());
class Ball extends BodyComponent with ContactCallbacks {
...
@override
Body createBody() {
...
final bodyDef = BodyDef(
userData = this,
);
...
}
}
```

Every time `Ball` and `Wall` gets in contact `begin` will be called, and once the objects stop being
in contact `end` will be called.

If you want an object to interact with all other bodies, put `BodyComponent` as the one of the
parameters of your `ContactCallback` like this:

`class BallAnythingCallback implements ContactCallback<Ball, BodyComponent> ...`
Every time `Ball` and `Wall` begin to come in contact `beginContact` will be called, and once the
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_forge2d/blob/main/example).
[Flame Forge2D example](https://github.com/flame-engine/flame/blob/main/packages/flame_forge2d/example/lib/balls.dart).


### Forge2DCamera.followBodyComponent
Expand Down
46 changes: 22 additions & 24 deletions packages/flame_forge2d/example/lib/balls.dart
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter/material.dart';

import 'boundaries.dart';

class Ball extends BodyComponent {
class Ball extends BodyComponent with ContactCallbacks {
late Paint originalPaint;
bool giveNudge = false;
final double radius;
Expand Down Expand Up @@ -62,39 +62,37 @@ class Ball extends BodyComponent {
}
}
}
}

class WhiteBall extends Ball {
WhiteBall(Vector2 position) : super(position) {
originalPaint = BasicPalette.white.paint();
paint = originalPaint;
}
}

class BallContactCallback extends ContactCallback<Ball, Ball> {
@override
void begin(Ball ball1, Ball ball2, Contact contact) {
if (ball1 is WhiteBall || ball2 is WhiteBall) {
void beginContact(Object other, Contact contact) {
if (other is Wall) {
other.paint = paint;
}

if (other is WhiteBall) {
return;
}
if (ball1.paint != ball1.originalPaint) {
ball1.paint = ball2.paint;
} else {
ball2.paint = ball1.paint;

if (other is Ball) {
if (paint != originalPaint) {
paint = other.paint;
} else {
other.paint = paint;
}
}
}
}

class WhiteBallContactCallback extends ContactCallback<Ball, WhiteBall> {
@override
void begin(Ball ball, WhiteBall whiteBall, Contact contact) {
ball.giveNudge = true;
class WhiteBall extends Ball with ContactCallbacks {
WhiteBall(Vector2 position) : super(position) {
originalPaint = BasicPalette.white.paint();
paint = originalPaint;
}
}

class BallWallContactCallback extends ContactCallback<Ball, Wall> {
@override
void begin(Ball ball, Wall wall, Contact contact) {
wall.paint = ball.paint;
void beginContact(Object other, Contact contact) {
if (other is Ball) {
other.giveNudge = true;
}
}
}
Expand Up @@ -19,9 +19,6 @@ that it collides with.
Future<void> onLoad() async {
final boundaries = createBoundaries(this);
boundaries.forEach(add);
addContactCallback(BallContactCallback());
addContactCallback(BallWallContactCallback());
addContactCallback(WhiteBallContactCallback());
}

@override
Expand Down
120 changes: 39 additions & 81 deletions packages/flame_forge2d/lib/contact_callbacks.dart
@@ -1,93 +1,51 @@
import 'package:forge2d/forge2d.dart';

import 'body_component.dart';

class ContactTypes<T1, T2> {
// If o1 is, or inherits from, T1 or T2
bool has(Object o1) => o1 is T1 || o1 is T2;
bool hasOne(Object o1, Object o2) => has(o1) || has(o2);

// Only makes sense to call with objects that you know is in [T1, T2]
bool inOrder(Object o1, Object o2) => o1 is T1 && o2 is T2;

// Remember that this is not symmetric, it checks if the types in `o1` and
// `o2` are the same or inherits from the types in `other`
bool match(Object o1, Object o2) =>
(o1 is T1 && o2 is T2) || (o2 is T1 && o1 is T2);
}

typedef ContactCallbackFun = void Function(Object a, Object b, Contact contact);

abstract class ContactCallback<Type1, Type2> {
ContactTypes<Type1, Type2> types = ContactTypes<Type1, Type2>();

void begin(Type1 a, Type2 b, Contact contact) {}
void end(Type1 a, Type2 b, Contact contact) {}
void preSolve(Type1 a, Type2 b, Contact contact, Manifold oldManifold) {}
void postSolve(Type1 a, Type2 b, Contact contact, ContactImpulse impulse) {}
}

class ContactCallbacks extends ContactListener {
final List<ContactCallback> _callbacks = [];

void register(ContactCallback callback) {
_callbacks.add(callback);
import 'flame_forge2d.dart';

/// Used to listen to a [BodyComponent]'s contact events.
///
/// Contact events occur whenever two [Fixture] meet each other.
///
/// To react to contacts you should assign [ContactCallbacks] to a [Body]'s
/// userData or/and to a [Fixture]'s userData.
/// {@macro flame_forge2d.world_contact_listener.algorithm}
class ContactCallbacks {
/// Called when two [Fixture]s start being in contact.
///
/// It is called for sensors and non-sensors.
void beginContact(Object other, Contact contact) {
onBeginContact?.call(other, contact);
}

void deregister(ContactCallback callback) {
_callbacks.remove(callback);
/// Called when two [Fixture]s cease being in contact.
///
/// It is called for sensors and non-sensors.
void endContact(Object other, Contact contact) {
onEndContact?.call(other, contact);
}

void clear() {
_callbacks.clear();
/// Called after collision detection, but before collision resolution.
///
/// This gives you a chance to disable the [Contact] based on the current
/// configuration.
/// Sensors do not create [Manifold]s.
void preSolve(Object other, Contact contact, Manifold oldManifold) {
onPreSolve?.call(other, contact, oldManifold);
}

void _maybeCallback(
Contact contact,
ContactCallback callback,
ContactCallbackFun f,
) {
final a = contact.fixtureA.body.userData;
final b = contact.fixtureB.body.userData;
final wanted = callback.types;

if (a == null || b == null) {
return;
}

if (wanted.match(a, b) ||
(wanted.has(BodyComponent) && wanted.hasOne(a, b))) {
wanted.inOrder(a, b) ? f(a, b, contact) : f(b, a, contact);
}
/// Called after collision resolution.
///
/// Usually defined to gather collision impulse results.
/// If one of the colliding objects is a sensor, this will not be called.
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
onPostSolve?.call(other, contact, impulse);
}

@override
void beginContact(Contact contact) =>
_callbacks.forEach((c) => _maybeCallback(contact, c, c.begin));
void Function(Object other, Contact contact)? onBeginContact;

@override
void endContact(Contact contact) =>
_callbacks.forEach((c) => _maybeCallback(contact, c, c.end));
void Function(Object other, Contact contact)? onEndContact;

@override
void preSolve(Contact contact, Manifold oldManifold) {
_callbacks.forEach((c) {
void preSolveAux(Object a, Object b, Contact contact) {
c.preSolve(a, b, contact, oldManifold);
}
void Function(Object other, Contact contact, Manifold oldManifold)?
onPreSolve;

_maybeCallback(contact, c, preSolveAux);
});
}

@override
void postSolve(Contact contact, ContactImpulse impulse) {
_callbacks.forEach((c) {
void postSolveAux(Object a, Object b, Contact contact) {
c.postSolve(a, b, contact, impulse);
}

_maybeCallback(contact, c, postSolveAux);
});
}
void Function(Object other, Contact contact, ContactImpulse impulse)?
onPostSolve;
}
24 changes: 6 additions & 18 deletions packages/flame_forge2d/lib/forge2d_game.dart
@@ -1,44 +1,32 @@
import 'package:flame/game.dart';
import 'package:forge2d/forge2d.dart' hide Timer;
import 'package:forge2d/forge2d.dart';

import 'contact_callbacks.dart';
import 'world_contact_listener.dart';

class Forge2DGame extends FlameGame {
Forge2DGame({
Vector2? gravity,
double zoom = defaultZoom,
Camera? camera,
ContactListener? contactListener,
}) : world = World(gravity ?? defaultGravity),
super(camera: camera ?? Camera()) {
this.camera.zoom = zoom;
world.setContactListener(_contactCallbacks);
world.setContactListener(contactListener ?? WorldContactListener());
}

static final Vector2 defaultGravity = Vector2(0, 10.0);
static final Vector2 defaultGravity = Vector2(0, -10.0);

static const double defaultZoom = 10.0;

final World world;

final ContactCallbacks _contactCallbacks = ContactCallbacks();

@override
void update(double dt) {
super.update(dt);
world.stepDt(dt);
}

void addContactCallback(ContactCallback callback) {
_contactCallbacks.register(callback);
}

void removeContactCallback(ContactCallback callback) {
_contactCallbacks.deregister(callback);
}

void clearContactCallbacks() {
_contactCallbacks.clear();
}

Vector2 worldToScreen(Vector2 position) {
return projector.projectVector(position);
}
Expand Down

0 comments on commit a50d4a1

Please sign in to comment.