From f2a20742f0429e0f372bfabb81833864645cb2fd Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Sat, 30 Apr 2022 17:29:12 +0100 Subject: [PATCH] feat: allow targeting fixtures in `ContactBehavior` (#263) --- .../lib/src/contact_behavior.dart | 104 +++++----- .../test/src/contact_behavior_test.dart | 181 ++++++++++++++---- 2 files changed, 199 insertions(+), 86 deletions(-) diff --git a/packages/pinball_flame/lib/src/contact_behavior.dart b/packages/pinball_flame/lib/src/contact_behavior.dart index 79112398..ff715b12 100644 --- a/packages/pinball_flame/lib/src/contact_behavior.dart +++ b/packages/pinball_flame/lib/src/contact_behavior.dart @@ -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 extends Component with ContactCallbacks, ParentIsA { + final _fixturesUserData = {}; + + /// 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 userData) => _fixturesUserData.addAll(userData); + @override - @mustCallSuper Future 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 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 = []; +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 _userData; + + Iterable get _contactCallbacks => + _userData.whereType(); + + 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; } diff --git a/packages/pinball_flame/test/src/contact_behavior_test.dart b/packages/pinball_flame/test/src/contact_behavior_test.dart index 630156ed..cf7fe35a 100644 --- a/packages/pinball_flame/test/src/contact_behavior_test.dart +++ b/packages/pinball_flame/test/src/contact_behavior_test.dart @@ -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()); }, ); 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()); + 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()); + expect(fixture2.userData, isA()); + }, + ); + + 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)); + }, + ); }); }