From 6d92eb3f1d6a0bdd9589ab108c93e8626e212907 Mon Sep 17 00:00:00 2001 From: alestiago Date: Tue, 19 Apr 2022 11:30:31 +0100 Subject: [PATCH] feat: defined ContactCallbacks2 --- packages/pinball_flame/lib/pinball_flame.dart | 1 + .../lib/src/contact_callbacks.dart | 86 +++++ .../test/src/contact_callbacks.dart | 364 ++++++++++++++++++ 3 files changed, 451 insertions(+) create mode 100644 packages/pinball_flame/lib/src/contact_callbacks.dart create mode 100644 packages/pinball_flame/test/src/contact_callbacks.dart diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 2d2e760b..a4bf3af6 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -2,4 +2,5 @@ library pinball_flame; export 'src/blueprint.dart'; export 'src/component_controller.dart'; +export 'src/contact_callbacks.dart'; export 'src/keyboard_input_controller.dart'; diff --git a/packages/pinball_flame/lib/src/contact_callbacks.dart b/packages/pinball_flame/lib/src/contact_callbacks.dart new file mode 100644 index 00000000..3dda2462 --- /dev/null +++ b/packages/pinball_flame/lib/src/contact_callbacks.dart @@ -0,0 +1,86 @@ +// TODO(alestiago): Remove once the below is merged and updated. +// https://github.com/flame-engine/flame/pull/1547 + +// ignore_for_file: public_member_api_docs +// ignore_for_file: avoid_renaming_method_parameters +import 'package:flame_forge2d/flame_forge2d.dart'; + +abstract class ContactCallbacks2 { + void beginContact(Object other, Contact contact) {} + void endContact(Object other, Contact contact) {} + void preSolve(Object other, Contact contact, Manifold oldManifold) {} + void postSolve(Object other, Contact contact, ContactImpulse impulse) {} +} + +/// Listens to the entire [World] contacts events. +/// +/// It propagates contact events ([beginContact], [endContact], [preSolve], +/// [postSolve]) to other [ContactCallbacks]s when a [Body] or at least one of +/// its fixtures `userData` is set to a [ContactCallbacks]. +/// +/// If the [Body] `userData` is set to a [ContactCallbacks] the contact events +/// of this will be called to when any [Body]'s fixture contacts another +/// [Fixture]. +/// +/// If instead you wish to be more specific and only trigger contact events +/// when a specific [Body]'s fixture contacts with another [Fixture] you can +/// set the fixture `userData` to a [ContactCallbacks]. +/// +/// The described behaviour is a simple out of the box solution to propagate +/// contact events. If you wish to implement your own logic you can subclass +/// [ContactListener] and provide it to your [Forge2DGame]. +class WorldContactListener extends ContactListener { + void _callback( + Contact contact, + void Function(ContactCallbacks2 contactCallback, Object other) callback, + ) { + final userDatas = { + contact.bodyA.userData, + contact.fixtureA.userData, + contact.bodyB.userData, + contact.fixtureB.userData, + }.whereType(); + + for (final contactCallback in userDatas.whereType()) { + for (final object in userDatas) { + if (object != contactCallback) { + callback(contactCallback, object); + } + } + } + } + + @override + void beginContact(Contact contact) { + _callback( + contact, + (contactCallback, other) => contactCallback.beginContact(other, contact), + ); + } + + @override + void endContact(Contact contact) { + _callback( + contact, + (contactCallback, other) => contactCallback.endContact(other, contact), + ); + } + + @override + void preSolve(Contact contact, Manifold oldManifold) { + _callback( + contact, + (contactCallback, other) => + contactCallback.preSolve(other, contact, oldManifold), + ); + } + + @override + void postSolve(Contact contact, ContactImpulse contactImpulse) { + _callback( + contact, + (contactCallback, other) => + contactCallback.postSolve(other, contact, contactImpulse), + ); + } +} diff --git a/packages/pinball_flame/test/src/contact_callbacks.dart b/packages/pinball_flame/test/src/contact_callbacks.dart new file mode 100644 index 00000000..eb36cca5 --- /dev/null +++ b/packages/pinball_flame/test/src/contact_callbacks.dart @@ -0,0 +1,364 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; +import 'package:test/scaffolding.dart'; + +class MockContactCallback extends Mock implements ContactCallbacks2 {} + +class MockContact extends Mock implements Contact {} + +class MockBody extends Mock implements Body {} + +class MockFixture extends Mock implements Fixture {} + +class MockManifold extends Mock implements Manifold {} + +class MockContactImpulse extends Mock implements ContactImpulse {} + +void main() { + group( + 'WorldContactListener', + () { + late ContactCallbacks2 contactCallback; + late Contact contact; + late Body bodyA; + late Body bodyB; + late Fixture fixtureA; + late Fixture fixtureB; + + setUp(() { + contactCallback = MockContactCallback(); + contact = MockContact(); + bodyA = MockBody(); + bodyB = MockBody(); + fixtureA = MockFixture(); + fixtureB = MockFixture(); + + when(() => contact.bodyA).thenReturn(bodyA); + when(() => contact.bodyB).thenReturn(bodyB); + when(() => contact.fixtureA).thenReturn(fixtureA); + when(() => contact.fixtureB).thenReturn(fixtureB); + }); + + setUpAll(() { + registerFallbackValue(Object()); + }); + + group( + 'preSolve', + () { + late Manifold manifold; + + setUp(() { + manifold = MockManifold(); + }); + + test( + "doesn't callback if userData are null", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(null); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.preSolve(contact, manifold); + + verifyNever( + () => contactCallback.preSolve(any(), contact, manifold), + ); + }, + ); + + test( + 'callbacks for userData when not null', + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(Object()); + when(() => fixtureA.userData).thenReturn(Object()); + when(() => fixtureB.userData).thenReturn(Object()); + + contactListener.preSolve(contact, manifold); + + verify( + () => contactCallback.preSolve( + bodyB.userData!, + contact, + manifold, + ), + ).called(1); + verify( + () => contactCallback.preSolve( + fixtureA.userData!, + contact, + manifold, + ), + ).called(1); + verify( + () => contactCallback.preSolve( + fixtureB.userData!, + contact, + manifold, + ), + ).called(1); + verify( + () => contactCallback.preSolve( + any(), + contact, + manifold, + ), + ).called(3); + }, + ); + + test( + "doesn't callback itself", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(contactCallback); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.preSolve(contact, manifold); + + verifyNever( + () => contactCallback.preSolve(any(), contact, manifold), + ); + }, + ); + }, + ); + + group( + 'beginContact', + () { + test( + "doesn't callback if userData are null", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(null); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.beginContact(contact); + + verifyNever( + () => contactCallback.beginContact(any(), contact), + ); + }, + ); + + test( + 'callbacks for userData when not null', + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(Object()); + when(() => fixtureA.userData).thenReturn(Object()); + when(() => fixtureB.userData).thenReturn(Object()); + + contactListener.beginContact(contact); + + verify( + () => contactCallback.beginContact(bodyB.userData!, contact), + ).called(1); + verify( + () => contactCallback.beginContact(fixtureA.userData!, contact), + ).called(1); + verify( + () => contactCallback.beginContact(fixtureB.userData!, contact), + ).called(1); + verify( + () => contactCallback.beginContact(any(), contact), + ).called(3); + }, + ); + + test( + "doesn't callback itself", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(contactCallback); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.beginContact(contact); + + verifyNever( + () => contactCallback.beginContact(any(), contact), + ); + }, + ); + }, + ); + + group( + 'endContact', + () { + test( + "doesn't callback if userData are null", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(null); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.endContact(contact); + + verifyNever( + () => contactCallback.endContact(any(), contact), + ); + }, + ); + + test( + 'callbacks for userData when not null', + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(Object()); + when(() => fixtureA.userData).thenReturn(Object()); + when(() => fixtureB.userData).thenReturn(Object()); + + contactListener.endContact(contact); + + verify( + () => contactCallback.endContact(bodyB.userData!, contact), + ).called(1); + verify( + () => contactCallback.endContact(fixtureA.userData!, contact), + ).called(1); + verify( + () => contactCallback.endContact(fixtureB.userData!, contact), + ).called(1); + verify( + () => contactCallback.endContact(any(), contact), + ).called(3); + }, + ); + + test( + "doesn't callback itself", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(contactCallback); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.endContact(contact); + + verifyNever( + () => contactCallback.endContact(any(), contact), + ); + }, + ); + }, + ); + + group( + 'postSolve', + () { + late ContactImpulse contactImpulse; + + setUp(() { + contactImpulse = MockContactImpulse(); + }); + + test( + "doesn't callback if userData are null", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(null); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.postSolve(contact, contactImpulse); + + verifyNever( + () => contactCallback.postSolve(any(), contact, contactImpulse), + ); + }, + ); + + test( + 'callbacks for userData when not null', + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(Object()); + when(() => fixtureA.userData).thenReturn(Object()); + when(() => fixtureB.userData).thenReturn(Object()); + + contactListener.postSolve(contact, contactImpulse); + + verify( + () => contactCallback.postSolve( + bodyB.userData!, + contact, + contactImpulse, + ), + ).called(1); + verify( + () => contactCallback.postSolve( + fixtureA.userData!, + contact, + contactImpulse, + ), + ).called(1); + verify( + () => contactCallback.postSolve( + fixtureB.userData!, + contact, + contactImpulse, + ), + ).called(1); + verify( + () => contactCallback.postSolve( + any(), + contact, + contactImpulse, + ), + ).called(3); + }, + ); + + test( + "doesn't callback itself", + () { + final contactListener = WorldContactListener(); + + when(() => bodyA.userData).thenReturn(contactCallback); + when(() => bodyB.userData).thenReturn(contactCallback); + when(() => fixtureA.userData).thenReturn(null); + when(() => fixtureB.userData).thenReturn(null); + + contactListener.postSolve(contact, contactImpulse); + + verifyNever( + () => contactCallback.postSolve(any(), contact, contactImpulse), + ); + }, + ); + }, + ); + }, + ); +}