From f1b35d3eb217ed4941c9c4523309eeb0b942b77c Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 23 Mar 2022 23:08:15 +0100 Subject: [PATCH 1/3] feat: create ellipses from geometry (#84) * feat: create ellipses from geometry * test: geometry test for ellipse * feat: removed required angle and added tests * test: completed tests for geometry * chore: unused import * Update lib/game/components/pathway.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: renaming params * chore: missed test saved Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/pathway.dart | 40 ++++++++++++++ lib/game/pinball_game.dart | 1 - packages/geometry/lib/src/geometry.dart | 53 +++++++++++++++---- packages/geometry/test/src/geometry_test.dart | 40 ++++++++++++++ test/game/components/pathway_test.dart | 36 +++++++++++++ 5 files changed, 159 insertions(+), 11 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 8604e0f3..819ed5f4 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -144,6 +144,46 @@ class Pathway extends BodyComponent with InitialPosition, Layered { ); } + /// Creates an ellipse [Pathway]. + /// + /// Does so with two [ChainShape]s separated by a [width]. Can + /// be rotated by a given [rotation] in radians. + /// + /// If [singleWall] is true, just one [ChainShape] is created. + factory Pathway.ellipse({ + Color? color, + required Vector2 center, + required double width, + required double majorRadius, + required double minorRadius, + double rotation = 0, + bool singleWall = false, + }) { + final paths = >[]; + + // TODO(ruialonso): Refactor repetitive logic + final outerWall = calculateEllipse( + center: center, + majorRadius: majorRadius, + minorRadius: minorRadius, + ).map((vector) => vector..rotate(rotation)).toList(); + paths.add(outerWall); + + if (!singleWall) { + final innerWall = calculateEllipse( + center: center, + majorRadius: majorRadius - width, + minorRadius: minorRadius - width, + ).map((vector) => vector..rotate(rotation)).toList(); + paths.add(innerWall); + } + + return Pathway._( + color: color, + paths: paths, + ); + } + final List> _paths; /// Constructs different [ChainShape]s to form the [Pathway] shape. diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 44a7ec01..3ce7fd77 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs - import 'dart:async'; import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 8574bc73..6975f8cb 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -23,10 +23,45 @@ List calculateArc({ final points = []; for (var i = 0; i < precision; i++) { - final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); - final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); + final x = center.x + radius * math.cos((stepAngle * i) + offsetAngle); + final y = center.y - radius * math.sin((stepAngle * i) + offsetAngle); - final point = Vector2(xCoord, yCoord); + final point = Vector2(x, y); + points.add(point); + } + + return points; +} + +/// Calculates all [Vector2]s of an ellipse. +/// +/// An ellipse can be achieved by specifying a [center], a [majorRadius] and a +/// [minorRadius]. +/// +/// The higher the [precision], the more [Vector2]s will be calculated; +/// achieving a more rounded ellipse. +/// +/// For more information read: https://en.wikipedia.org/wiki/Ellipse. +List calculateEllipse({ + required Vector2 center, + required double majorRadius, + required double minorRadius, + int precision = 100, +}) { + assert( + 0 < minorRadius && minorRadius <= majorRadius, + 'smallRadius ($minorRadius) and bigRadius ($majorRadius) must be in ' + 'range 0 < smallRadius <= bigRadius', + ); + + final stepAngle = 2 * math.pi / (precision - 1); + + final points = []; + for (var i = 0; i < precision; i++) { + final x = center.x + minorRadius * math.cos(stepAngle * i); + final y = center.y - majorRadius * math.sin(stepAngle * i); + + final point = Vector2(x, y); points.add(point); } @@ -63,17 +98,15 @@ List calculateBezierCurve({ final points = []; do { - var xCoord = 0.0; - var yCoord = 0.0; + var x = 0.0; + var y = 0.0; for (var i = 0; i <= n; i++) { final point = controlPoints[i]; - xCoord += - binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; - yCoord += - binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; + x += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; + y += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; } - points.add(Vector2(xCoord, yCoord)); + points.add(Vector2(x, y)); t = t + step; } while (t <= 1); diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 5c33d70f..7a49b2b2 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -33,6 +33,46 @@ void main() { }); }); + group('calculateEllipse', () { + test('returns by default 100 points as indicated by precision', () { + final points = calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 50, + ); + expect(points.length, 100); + }); + + test('returns as many points as indicated by precision', () { + final points = calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 50, + precision: 50, + ); + expect(points.length, 50); + }); + + test('fails if radius not in range', () { + expect( + () => calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 150, + ), + throwsA(isA()), + ); + expect( + () => calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 0, + ), + throwsA(isA()), + ); + }); + }); + group('calculateBezierCurve', () { test('fails if step not in range', () { expect( diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 03b67c62..63e74d4d 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -165,6 +165,42 @@ void main() { }); }); + group('ellipse', () { + flameTester.test( + 'loads correctly', + (game) async { + final pathway = Pathway.ellipse( + center: Vector2.zero(), + width: width, + majorRadius: 150, + minorRadius: 70, + ); + await game.ready(); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'is static', + (game) async { + final pathway = Pathway.ellipse( + center: Vector2.zero(), + width: width, + majorRadius: 150, + minorRadius: 70, + ); + await game.ready(); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + group('bezier curve', () { final controlPoints = [ Vector2(0, 0), From d5d3640f0ac58b1c7ebd8c1474a677d808ead51a Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 24 Mar 2022 08:50:35 +0000 Subject: [PATCH 2/3] refactor: define `FlipperJoint` (#72) * refactor: defined FlipperJoint * refactor: simplified logic * refactor: removed tests * docs: included TODO comment * refactor: simplified shape logic * docs: included asset TODO comment * refactor: removed verbose constructors * refactor: reordered classes * refactor: used renderBody * chore: removed unused import * refactor: moved renderBody to onLoad --- lib/game/components/board.dart | 2 +- lib/game/components/flipper.dart | 135 ++++++++----------- test/game/components/flipper_test.dart | 178 ++----------------------- 3 files changed, 67 insertions(+), 248 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index af03efdd..efa3f137 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -127,7 +127,7 @@ class _BottomGroupSide extends Component { Future onLoad() async { final direction = _side.direction; - final flipper = Flipper.fromSide( + final flipper = Flipper( side: _side, )..initialPosition = _position; final baseboard = Baseboard(side: _side) diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 92b2ddd4..48913934 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -3,11 +3,20 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; +const _leftFlipperKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, +]; + +const _rightFlipperKeys = [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, +]; + /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. /// @@ -15,43 +24,9 @@ import 'package:pinball/gen/assets.gen.dart'; /// {@endtemplate flipper} class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// {@macro flipper} - Flipper._({ + Flipper({ required this.side, - required List keys, - }) : _keys = keys; - - Flipper._left() - : this._( - side: BoardSide.left, - keys: [ - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.keyA, - ], - ); - - Flipper._right() - : this._( - side: BoardSide.right, - keys: [ - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.keyD, - ], - ); - - /// Constructs a [Flipper] from a [BoardSide]. - /// - /// A [Flipper._right] and [Flipper._left] besides being mirrored - /// horizontally, also have different [LogicalKeyboardKey]s that control them. - factory Flipper.fromSide({ - required BoardSide side, - }) { - switch (side) { - case BoardSide.left: - return Flipper._left(); - case BoardSide.right: - return Flipper._right(); - } - } + }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys; /// The size of the [Flipper]. static final size = Vector2(12, 2.8); @@ -104,35 +79,29 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. Future _anchorToJoint() async { - final anchor = FlipperAnchor(flipper: this); + final anchor = _FlipperAnchor(flipper: this); await add(anchor); - final jointDef = FlipperAnchorRevoluteJointDef( + final jointDef = _FlipperAnchorRevoluteJointDef( flipper: this, anchor: anchor, ); - // TODO(alestiago): Remove casting once the following is closed: - // https://github.com/flame-engine/forge2d/issues/36 - final joint = world.createJoint(jointDef) as RevoluteJoint; + final joint = _FlipperJoint(jointDef)..create(world); // FIXME(erickzanardo): when mounted the initial position is not fully // reached. unawaited( - mounted.whenComplete( - () => FlipperAnchorRevoluteJointDef.unlock(joint, side), - ), + mounted.whenComplete(joint.unlock), ); } List _createFixtureDefs() { final fixturesDef = []; - final isLeft = side.isLeft; + final direction = side.direction; final bigCircleShape = CircleShape()..radius = 1.75; bigCircleShape.position.setValues( - isLeft - ? -(size.x / 2) + bigCircleShape.radius - : (size.x / 2) - bigCircleShape.radius, + ((size.x / 2) * direction) + (bigCircleShape.radius * -direction), 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); @@ -140,15 +109,13 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final smallCircleShape = CircleShape()..radius = 0.9; smallCircleShape.position.setValues( - isLeft - ? (size.x / 2) - smallCircleShape.radius - : -(size.x / 2) + smallCircleShape.radius, + ((size.x / 2) * -direction) + (smallCircleShape.radius * direction), 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); fixturesDef.add(smallCircleFixtureDef); - final trapeziumVertices = isLeft + final trapeziumVertices = side.isLeft ? [ Vector2(bigCircleShape.position.x, bigCircleShape.radius), Vector2(smallCircleShape.position.x, smallCircleShape.radius), @@ -173,7 +140,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { @override Future onLoad() async { await super.onLoad(); - paint = Paint()..color = Colors.transparent; + renderBody = false; + await Future.wait([ _loadSprite(), _anchorToJoint(), @@ -214,61 +182,66 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// /// The end of a [Flipper] depends on its [Flipper.side]. /// {@endtemplate} -class FlipperAnchor extends JointAnchor { +class _FlipperAnchor extends JointAnchor { /// {@macro flipper_anchor} - FlipperAnchor({ + _FlipperAnchor({ required Flipper flipper, }) { initialPosition = Vector2( - flipper.side.isLeft - ? flipper.body.position.x - Flipper.size.x / 2 - : flipper.body.position.x + Flipper.size.x / 2, + flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2), flipper.body.position.y, ); } } /// {@template flipper_anchor_revolute_joint_def} -/// Hinges one end of [Flipper] to a [FlipperAnchor] to achieve an arc motion. +/// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion. /// {@endtemplate} -class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { +class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { /// {@macro flipper_anchor_revolute_joint_def} - FlipperAnchorRevoluteJointDef({ + _FlipperAnchorRevoluteJointDef({ required Flipper flipper, - required FlipperAnchor anchor, - }) { + required _FlipperAnchor anchor, + }) : side = flipper.side { initialize( flipper.body, anchor.body, anchor.body.position, ); - enableLimit = true; - final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2; + enableLimit = true; + final angle = (_sweepingAngle * -side.direction) / 2; lowerAngle = upperAngle = angle; } /// The total angle of the arc motion. static const _sweepingAngle = math.pi / 3.5; + final BoardSide side; +} + +class _FlipperJoint extends RevoluteJoint { + _FlipperJoint(_FlipperAnchorRevoluteJointDef def) + : side = def.side, + super(def); + + final BoardSide side; + + // TODO(alestiago): Remove once Forge2D supports custom joints. + void create(World world) { + world.joints.add(this); + bodyA.joints.add(this); + bodyB.joints.add(this); + } + /// Unlocks the [Flipper] from its resting position. /// /// The [Flipper] is locked when initialized in order to force it to be at /// its resting position. - // TODO(alestiago): consider refactor once the issue is solved: - // https://github.com/flame-engine/forge2d/issues/36 - static void unlock(RevoluteJoint joint, BoardSide side) { - late final double upperLimit, lowerLimit; - switch (side) { - case BoardSide.left: - lowerLimit = -joint.lowerLimit; - upperLimit = joint.upperLimit; - break; - case BoardSide.right: - lowerLimit = joint.lowerLimit; - upperLimit = -joint.upperLimit; - } - - joint.setLimits(lowerLimit, upperLimit); + void unlock() { + setLimits( + lowerLimit * side.direction, + -upperLimit * side.direction, + ); } } diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 64d2f77b..3c12e37e 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -17,13 +17,14 @@ void main() { group( 'Flipper', () { + // TODO(alestiago): Add golden tests. flameTester.test( 'loads correctly', (game) async { - final leftFlipper = Flipper.fromSide( + final leftFlipper = Flipper( side: BoardSide.left, ); - final rightFlipper = Flipper.fromSide( + final rightFlipper = Flipper( side: BoardSide.right, ); await game.ready(); @@ -36,13 +37,13 @@ void main() { group('constructor', () { test('sets BoardSide', () { - final leftFlipper = Flipper.fromSide( + final leftFlipper = Flipper( side: BoardSide.left, ); expect(leftFlipper.side, equals(leftFlipper.side)); - final rightFlipper = Flipper.fromSide( + final rightFlipper = Flipper( side: BoardSide.right, ); expect(rightFlipper.side, equals(rightFlipper.side)); @@ -53,7 +54,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -65,7 +66,7 @@ void main() { flameTester.test( 'ignores gravity', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -77,7 +78,7 @@ void main() { flameTester.test( 'has greater mass than Ball', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); final ball = Ball(); @@ -97,7 +98,7 @@ void main() { flameTester.test( 'has three', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -109,7 +110,7 @@ void main() { flameTester.test( 'has density', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -139,7 +140,7 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.fromSide( + flipper = Flipper( side: BoardSide.left, ); }); @@ -205,7 +206,7 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.fromSide( + flipper = Flipper( side: BoardSide.right, ); }); @@ -269,159 +270,4 @@ void main() { }); }, ); - - group('FlipperAnchor', () { - flameTester.test( - 'position is at the left of the left Flipper', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - expect(flipperAnchor.body.position.x, equals(-Flipper.size.x / 2)); - }, - ); - - flameTester.test( - 'position is at the right of the right Flipper', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.right, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - expect(flipperAnchor.body.position.x, equals(Flipper.size.x / 2)); - }, - ); - }); - - group('FlipperAnchorRevoluteJointDef', () { - group('initializes with', () { - flameTester.test( - 'limits enabled', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - final jointDef = FlipperAnchorRevoluteJointDef( - flipper: flipper, - anchor: flipperAnchor, - ); - - expect(jointDef.enableLimit, isTrue); - }, - ); - - group('equal upper and lower limits', () { - flameTester.test( - 'when Flipper is left', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - final jointDef = FlipperAnchorRevoluteJointDef( - flipper: flipper, - anchor: flipperAnchor, - ); - - expect(jointDef.lowerAngle, equals(jointDef.upperAngle)); - }, - ); - - flameTester.test( - 'when Flipper is right', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.right, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - final jointDef = FlipperAnchorRevoluteJointDef( - flipper: flipper, - anchor: flipperAnchor, - ); - - expect(jointDef.lowerAngle, equals(jointDef.upperAngle)); - }, - ); - }); - }); - - group( - 'unlocks', - () { - flameTester.test( - 'when Flipper is left', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.left, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - final jointDef = FlipperAnchorRevoluteJointDef( - flipper: flipper, - anchor: flipperAnchor, - ); - final joint = game.world.createJoint(jointDef) as RevoluteJoint; - - FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side); - - expect( - joint.upperLimit, - isNot(equals(joint.lowerLimit)), - ); - }, - ); - - flameTester.test( - 'when Flipper is right', - (game) async { - final flipper = Flipper.fromSide( - side: BoardSide.right, - ); - await game.ensureAdd(flipper); - - final flipperAnchor = FlipperAnchor(flipper: flipper); - await game.ensureAdd(flipperAnchor); - - final jointDef = FlipperAnchorRevoluteJointDef( - flipper: flipper, - anchor: flipperAnchor, - ); - final joint = game.world.createJoint(jointDef) as RevoluteJoint; - - FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side); - - expect( - joint.upperLimit, - isNot(equals(joint.lowerLimit)), - ); - }, - ); - }, - ); - }); } From 394acd1802a8fa78f12b48a7db8b8fb3e42daccd Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 24 Mar 2022 11:28:43 +0000 Subject: [PATCH 3/3] refactor: included ScorePoint generics (#88) --- lib/game/components/score_points.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index da894652..97cea82a 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -6,7 +6,7 @@ import 'package:pinball/game/game.dart'; /// {@template score_points} /// Specifies the amount of points received on [Ball] collision. /// {@endtemplate} -mixin ScorePoints on BodyComponent { +mixin ScorePoints on BodyComponent { /// {@macro score_points} int get points;