feat: allow targeting fixtures in `ContactBehavior` (#263)

pull/269/head
Alejandro Santiago 3 years ago committed by GitHub
parent 0a1ae053d8
commit f2a20742f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,95 +1,105 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Appends a new [ContactCallbacks] to the parent.
///
/// This is a convenience class for adding a [ContactCallbacks] to the parent.
/// In constract with just adding a [ContactCallbacks] to the parent's body
/// userData, this class respects the previous [ContactCallbacks] in the
/// parent's body userData, if any. Hence, it avoids overriding any previous
/// [ContactCallbacks] in the parent.
/// In contrast with just assigning a [ContactCallbacks] to a userData, this
/// class respects the previous userData.
///
/// It does so by grouping the [ContactCallbacks] in a [_ContactCallbacksGroup],
/// and resetting the parent's userData accordingly.
/// It does so by grouping the userData in a [_UserData], and resetting the
/// parent's userData accordingly.
// TODO(alestiago): Make use of generics to infer the type of the contact.
// https://github.com/VGVentures/pinball/pull/234#discussion_r859182267
// TODO(alestiago): Consider if there is a need to support adjusting a fixture's
// userData.
class ContactBehavior<T extends BodyComponent> extends Component
with ContactCallbacks, ParentIsA<T> {
final _fixturesUserData = <Object>{};
/// Specifies which fixtures should be considered for contact.
///
/// Fixtures are identifiable by their userData.
///
/// If no fixtures are specified, the [ContactCallbacks] is applied to the
/// entire body, hence all fixtures are considered.
void applyTo(Iterable<Object> userData) => _fixturesUserData.addAll(userData);
@override
@mustCallSuper
Future<void> onLoad() async {
final userData = parent.body.userData;
if (userData is _ContactCallbacksGroup) {
userData.addContactCallbacks(this);
} else if (userData is ContactCallbacks) {
final contactCallbacksGroup = _ContactCallbacksGroup()
..addContactCallbacks(userData)
..addContactCallbacks(this);
parent.body.userData = contactCallbacksGroup;
if (_fixturesUserData.isNotEmpty) {
for (final fixture in _targetedFixtures) {
fixture.userData = _UserData.fromFixture(fixture)..add(this);
}
} else {
parent.body.userData = this;
parent.body.userData = _UserData.fromBody(parent.body)..add(this);
}
}
Iterable<Fixture> get _targetedFixtures =>
parent.body.fixtures.where((fixture) {
if (_fixturesUserData.contains(fixture.userData)) return true;
final userData = fixture.userData;
if (userData is _UserData) {
return _fixturesUserData.contains(userData.value);
}
return false;
});
}
class _ContactCallbacksGroup implements ContactCallbacks {
final List<ContactCallbacks> _contactCallbacks = [];
class _UserData with ContactCallbacks {
_UserData._(Object? userData) : _userData = [userData];
factory _UserData._fromUserData(Object? userData) {
if (userData is _UserData) return userData;
return _UserData._(userData);
}
factory _UserData.fromFixture(Fixture fixture) =>
_UserData._fromUserData(fixture.userData);
factory _UserData.fromBody(Body body) =>
_UserData._fromUserData(body.userData);
final List<Object?> _userData;
Iterable<ContactCallbacks> get _contactCallbacks =>
_userData.whereType<ContactCallbacks>();
Object? get value => _userData.first;
void add(Object? userData) => _userData.add(userData);
@override
@mustCallSuper
void beginContact(Object other, Contact contact) {
onBeginContact?.call(other, contact);
super.beginContact(other, contact);
for (final callback in _contactCallbacks) {
callback.beginContact(other, contact);
}
}
@override
@mustCallSuper
void endContact(Object other, Contact contact) {
onEndContact?.call(other, contact);
super.endContact(other, contact);
for (final callback in _contactCallbacks) {
callback.endContact(other, contact);
}
}
@override
@mustCallSuper
void preSolve(Object other, Contact contact, Manifold oldManifold) {
onPreSolve?.call(other, contact, oldManifold);
super.preSolve(other, contact, oldManifold);
for (final callback in _contactCallbacks) {
callback.preSolve(other, contact, oldManifold);
}
}
@override
@mustCallSuper
void postSolve(Object other, Contact contact, ContactImpulse impulse) {
onPostSolve?.call(other, contact, impulse);
super.postSolve(other, contact, impulse);
for (final callback in _contactCallbacks) {
callback.postSolve(other, contact, impulse);
}
}
void addContactCallbacks(ContactCallbacks callback) {
_contactCallbacks.add(callback);
}
@override
void Function(Object other, Contact contact)? onBeginContact;
@override
void Function(Object other, Contact contact)? onEndContact;
@override
void Function(Object other, Contact contact, ContactImpulse impulse)?
onPostSolve;
@override
void Function(Object other, Contact contact, Manifold oldManifold)?
onPreSolve;
}

@ -58,28 +58,78 @@ void main() {
late Contact contact;
late Manifold manifold;
late ContactImpulse contactImpulse;
late FixtureDef fixtureDef;
setUp(() {
other = Object();
contact = _MockContact();
manifold = _MockManifold();
contactImpulse = _MockContactImpulse();
fixtureDef = FixtureDef(CircleShape());
});
flameTester.test(
'should add a new ContactCallbacks to the parent',
"should add a new ContactCallbacks to the parent's body userData "
'when not applied to fixtures',
(game) async {
final parent = _TestBodyComponent();
final contactBehavior = ContactBehavior();
await parent.add(contactBehavior);
await game.ensureAdd(parent);
expect(parent.body.userData, contactBehavior);
expect(parent.body.userData, isA<ContactCallbacks>());
},
);
flameTester.test(
"should respect the previous ContactCallbacks in the parent's userData",
'should add a new ContactCallbacks to the targeted fixture ',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture1 =
parent.body.createFixture(fixtureDef..userData = 'foo');
final fixture2 = parent.body.createFixture(fixtureDef..userData = null);
final contactBehavior = ContactBehavior()
..applyTo(
[fixture1.userData!],
);
await parent.ensureAdd(contactBehavior);
expect(parent.body.userData, isNull);
expect(fixture1.userData, isA<ContactCallbacks>());
expect(fixture2.userData, isNull);
},
);
flameTester.test(
'should add a new ContactCallbacks to the targeted fixtures ',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture1 =
parent.body.createFixture(fixtureDef..userData = 'foo');
final fixture2 =
parent.body.createFixture(fixtureDef..userData = 'boo');
final contactBehavior = ContactBehavior()
..applyTo([
fixture1.userData!,
fixture2.userData!,
]);
await parent.ensureAdd(contactBehavior);
expect(parent.body.userData, isNull);
expect(fixture1.userData, isA<ContactCallbacks>());
expect(fixture2.userData, isA<ContactCallbacks>());
},
);
flameTester.test(
"should respect the previous ContactCallbacks in the parent's userData "
'when not applied to fixtures',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
@ -113,41 +163,94 @@ void main() {
},
);
flameTester.test('can group multiple ContactBehaviors and keep listening',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final contactBehavior1 = _TestContactBehavior();
final contactBehavior2 = _TestContactBehavior();
final contactBehavior3 = _TestContactBehavior();
await parent.ensureAddAll([
contactBehavior1,
contactBehavior2,
contactBehavior3,
]);
final contactCallbacks = parent.body.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
expect(contactBehavior1.beginContactCallsCount, equals(1));
expect(contactBehavior2.beginContactCallsCount, equals(1));
expect(contactBehavior3.beginContactCallsCount, equals(1));
contactCallbacks.endContact(other, contact);
expect(contactBehavior1.endContactCallsCount, equals(1));
expect(contactBehavior2.endContactCallsCount, equals(1));
expect(contactBehavior3.endContactCallsCount, equals(1));
contactCallbacks.preSolve(other, contact, manifold);
expect(contactBehavior1.preSolveContactCallsCount, equals(1));
expect(contactBehavior2.preSolveContactCallsCount, equals(1));
expect(contactBehavior3.preSolveContactCallsCount, equals(1));
contactCallbacks.postSolve(other, contact, contactImpulse);
expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1));
});
flameTester.test(
'can group multiple ContactBehaviors and keep listening',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final contactBehavior1 = _TestContactBehavior();
final contactBehavior2 = _TestContactBehavior();
final contactBehavior3 = _TestContactBehavior();
await parent.ensureAddAll([
contactBehavior1,
contactBehavior2,
contactBehavior3,
]);
final contactCallbacks = parent.body.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
expect(contactBehavior1.beginContactCallsCount, equals(1));
expect(contactBehavior2.beginContactCallsCount, equals(1));
expect(contactBehavior3.beginContactCallsCount, equals(1));
contactCallbacks.endContact(other, contact);
expect(contactBehavior1.endContactCallsCount, equals(1));
expect(contactBehavior2.endContactCallsCount, equals(1));
expect(contactBehavior3.endContactCallsCount, equals(1));
contactCallbacks.preSolve(other, contact, manifold);
expect(contactBehavior1.preSolveContactCallsCount, equals(1));
expect(contactBehavior2.preSolveContactCallsCount, equals(1));
expect(contactBehavior3.preSolveContactCallsCount, equals(1));
contactCallbacks.postSolve(other, contact, contactImpulse);
expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1));
},
);
flameTester.test(
'can group multiple ContactBehaviors and keep listening '
'when applied to a fixture',
(game) async {
final parent = _TestBodyComponent();
await game.ensureAdd(parent);
final fixture = parent.body.createFixture(fixtureDef..userData = 'foo');
final contactBehavior1 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
final contactBehavior2 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
final contactBehavior3 = _TestContactBehavior()
..applyTo(
[fixture.userData!],
);
await parent.ensureAddAll([
contactBehavior1,
contactBehavior2,
contactBehavior3,
]);
final contactCallbacks = fixture.userData! as ContactCallbacks;
contactCallbacks.beginContact(other, contact);
expect(contactBehavior1.beginContactCallsCount, equals(1));
expect(contactBehavior2.beginContactCallsCount, equals(1));
expect(contactBehavior3.beginContactCallsCount, equals(1));
contactCallbacks.endContact(other, contact);
expect(contactBehavior1.endContactCallsCount, equals(1));
expect(contactBehavior2.endContactCallsCount, equals(1));
expect(contactBehavior3.endContactCallsCount, equals(1));
contactCallbacks.preSolve(other, contact, manifold);
expect(contactBehavior1.preSolveContactCallsCount, equals(1));
expect(contactBehavior2.preSolveContactCallsCount, equals(1));
expect(contactBehavior3.preSolveContactCallsCount, equals(1));
contactCallbacks.postSolve(other, contact, contactImpulse);
expect(contactBehavior1.postSolveContactCallsCount, equals(1));
expect(contactBehavior2.postSolveContactCallsCount, equals(1));
expect(contactBehavior3.postSolveContactCallsCount, equals(1));
},
);
});
}

Loading…
Cancel
Save