Merge pull request #14 from VGVentures/feat/paths

feat: path component
pull/31/head
Rui Miguel Alonso 4 years ago committed by GitHub
commit 24edc0118a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,6 +2,7 @@ export 'anchor.dart';
export 'ball.dart';
export 'board_side.dart';
export 'flipper.dart';
export 'pathway.dart';
export 'plunger.dart';
export 'score_points.dart';
export 'wall.dart';

@ -0,0 +1,178 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart';
/// {@template pathway}
/// [Pathway] creates lines of various shapes.
///
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
/// {@endtemplate}
class Pathway extends BodyComponent {
Pathway._({
// TODO(ruialonso): remove color when assets added.
Color? color,
required Vector2 position,
required List<List<Vector2>> paths,
}) : _position = position,
_paths = paths {
paint = Paint()
..color = color ?? const Color.fromARGB(0, 0, 0, 0)
..style = PaintingStyle.stroke;
}
/// Creates a uniform unidirectional (straight) [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Placed
/// at a [position] between [start] and [end] points. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.straight({
Color? color,
required Vector2 position,
required Vector2 start,
required Vector2 end,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = [
start.clone(),
end.clone(),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = [
start + Vector2(width, 0),
end + Vector2(width, 0),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
/// Creates an arc [Pathway].
///
/// The [angle], in radians, specifies the size of the arc. For example, 2*pi
/// returns a complete circumference and minor angles a semi circumference.
///
/// The center of the arc is placed at [position].
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// The outer radius is specified by [radius], whilst the inner one is
/// equivalent to [radius] - [width].
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.arc({
Color? color,
required Vector2 position,
required double width,
required double radius,
required double angle,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateArc(
center: position,
radius: radius,
angle: angle,
offsetAngle: rotation,
);
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateArc(
center: position,
radius: radius - width,
angle: angle,
offsetAngle: rotation,
);
paths.add(innerWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
/// Creates a bezier curve [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// First and last [controlPoints] set the beginning and end of the curve,
/// inner points between them set its final shape.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.bezierCurve({
Color? color,
required Vector2 position,
required List<Vector2> controlPoints,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = calculateBezierCurve(controlPoints: controlPoints)
.map((vector) => vector..rotate(rotation))
.toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = calculateBezierCurve(
controlPoints: controlPoints
.map((vector) => vector + Vector2(width, -width))
.toList(),
).map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
final Vector2 _position;
final List<List<Vector2>> _paths;
@override
Body createBody() {
final bodyDef = BodyDef()
..type = BodyType.static
..position = _position;
final body = world.createBody(bodyDef);
for (final path in _paths) {
final chain = ChainShape()
..createChain(
path.map(gameRef.screenToWorld).toList(),
);
final fixtureDef = FixtureDef(chain);
body.createFixture(fixtureDef);
}
return body;
}
}

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';

@ -0,0 +1,7 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock

@ -0,0 +1,11 @@
# geometry
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Helper package to calculate points of lines, arcs and curves for the pathways of the ball.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,3 @@
library geometry;
export 'src/geometry.dart';

@ -0,0 +1,107 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
/// Calculates all [Vector2]s of a circumference.
///
/// A circumference can be achieved by specifying a [center] and a [radius].
/// In addition, a semi-circle can be achieved by specifying its [angle] and an
/// [offsetAngle] (both in radians).
///
/// The higher the [precision], the more [Vector2]s will be calculated;
/// achieving a more rounded arc.
///
/// For more information read: https://en.wikipedia.org/wiki/Trigonometric_functions.
List<Vector2> calculateArc({
required Vector2 center,
required double radius,
required double angle,
double offsetAngle = 0,
int precision = 100,
}) {
final stepAngle = angle / (precision - 1);
final points = <Vector2>[];
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 point = Vector2(xCoord, yCoord);
points.add(point);
}
return points;
}
/// Calculates all [Vector2]s of a bezier curve.
///
/// A bezier curve of [controlPoints] that say how to create this curve.
///
/// First and last points specify the beginning and the end respectively
/// of the curve. The inner points specify the shape of the curve and
/// its turning points.
///
/// The [step] must be between zero and one (inclusive), indicating the
/// precision to calculate the curve.
///
/// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
List<Vector2> calculateBezierCurve({
required List<Vector2> controlPoints,
double step = 0.001,
}) {
assert(
0 <= step && step <= 1,
'Step ($step) must be in range 0 <= step <= 1',
);
assert(
controlPoints.length >= 2,
'At least 2 control points needed to create a bezier curve',
);
var t = 0.0;
final n = controlPoints.length - 1;
final points = <Vector2>[];
do {
var xCoord = 0.0;
var yCoord = 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;
}
points.add(Vector2(xCoord, yCoord));
t = t + step;
} while (t <= 1);
return points;
}
/// Calculates the binomial coefficient of 'n' and 'k'.
///
/// For more information read: https://en.wikipedia.org/wiki/Binomial_coefficient
num binomial(num n, num k) {
assert(0 <= k && k <= n, 'k ($k) and n ($n) must be in range 0 <= k <= n');
if (k == 0 || n == k) {
return 1;
} else {
return factorial(n) / (factorial(k) * factorial(n - k));
}
}
/// Calculate the factorial of 'n'.
///
/// For more information read: https://en.wikipedia.org/wiki/Factorial
num factorial(num n) {
assert(n >= 0, 'Factorial is not defined for negative number n ($n)');
if (n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

@ -0,0 +1,19 @@
name: geometry
description: Helper package to calculate points of lines, arcs and curves for the pathways of the ball
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flame: ^1.0.0
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.2.0
test: ^1.19.2
very_good_analysis: ^2.4.0

@ -0,0 +1,159 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'package:flame/extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:geometry/geometry.dart';
class Binomial {
Binomial({required this.n, required this.k});
final num n;
final num k;
}
void main() {
group('calculateArc', () {
test('returns by default 100 points as indicated by precision', () {
final points = calculateArc(
center: Vector2.zero(),
radius: 100,
angle: 90,
);
expect(points.length, 100);
});
test('returns as many points as indicated by precision', () {
final points = calculateArc(
center: Vector2.zero(),
radius: 100,
angle: 90,
precision: 50,
);
expect(points.length, 50);
});
});
group('calculateBezierCurve', () {
test('fails if step not in range', () {
expect(
() => calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
step: 2,
),
throwsAssertionError,
);
});
test('fails if not enough control points', () {
expect(
() => calculateBezierCurve(controlPoints: [Vector2.zero()]),
throwsAssertionError,
);
expect(
() => calculateBezierCurve(controlPoints: []),
throwsAssertionError,
);
});
test('returns by default 1000 points as indicated by step', () {
final points = calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
);
expect(points.length, 1000);
});
test('returns as many points as indicated by step', () {
final points = calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
step: 0.01,
);
expect(points.length, 100);
});
});
group('binomial', () {
test('fails if k is negative', () {
expect(() => binomial(1, -1), throwsAssertionError);
});
test('fails if n is negative', () {
expect(() => binomial(-1, 1), throwsAssertionError);
});
test('fails if n < k', () {
expect(() => binomial(1, 2), throwsAssertionError);
});
test('for a specific input gives a correct value', () {
final binomialInputsToExpected = {
Binomial(n: 0, k: 0): 1,
Binomial(n: 1, k: 0): 1,
Binomial(n: 1, k: 1): 1,
Binomial(n: 2, k: 0): 1,
Binomial(n: 2, k: 1): 2,
Binomial(n: 2, k: 2): 1,
Binomial(n: 3, k: 0): 1,
Binomial(n: 3, k: 1): 3,
Binomial(n: 3, k: 2): 3,
Binomial(n: 3, k: 3): 1,
Binomial(n: 4, k: 0): 1,
Binomial(n: 4, k: 1): 4,
Binomial(n: 4, k: 2): 6,
Binomial(n: 4, k: 3): 4,
Binomial(n: 4, k: 4): 1,
Binomial(n: 5, k: 0): 1,
Binomial(n: 5, k: 1): 5,
Binomial(n: 5, k: 2): 10,
Binomial(n: 5, k: 3): 10,
Binomial(n: 5, k: 4): 5,
Binomial(n: 5, k: 5): 1,
Binomial(n: 6, k: 0): 1,
Binomial(n: 6, k: 1): 6,
Binomial(n: 6, k: 2): 15,
Binomial(n: 6, k: 3): 20,
Binomial(n: 6, k: 4): 15,
Binomial(n: 6, k: 5): 6,
Binomial(n: 6, k: 6): 1,
};
binomialInputsToExpected.forEach((input, value) {
expect(binomial(input.n, input.k), value);
});
});
});
group('factorial', () {
test('fails if negative number', () {
expect(() => factorial(-1), throwsAssertionError);
});
test('for a specific input gives a correct value', () {
final factorialInputsToExpected = {
0: 1,
1: 1,
2: 2,
3: 6,
4: 24,
5: 120,
6: 720,
7: 5040,
8: 40320,
9: 362880,
10: 3628800,
11: 39916800,
12: 479001600,
13: 6227020800,
};
factorialInputsToExpected.forEach((input, expected) {
expect(factorial(input), expected);
});
});
});
}

@ -198,6 +198,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
geometry:
dependency: "direct main"
description:
path: "packages/geometry"
relative: true
source: path
version: "1.0.0+1"
glob:
dependency: transitive
description:

@ -17,6 +17,8 @@ dependencies:
flutter_bloc: ^8.0.1
flutter_localizations:
sdk: flutter
geometry:
path: packages/geometry
intl: ^0.17.0
pinball_theme:
path: packages/pinball_theme

@ -0,0 +1,255 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
group('Pathway', () {
const width = 50.0;
group('straight', () {
group('color', () {
flameTester.test(
'has transparent color by default when no color is specified',
(game) async {
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(
pathway.paint.color,
equals(Color.fromARGB(0, 0, 0, 0)),
);
},
);
flameTester.test(
'has a color when is specified',
(game) async {
const defaultColor = Colors.blue;
final pathway = Pathway.straight(
color: defaultColor,
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(pathway.paint.color.value, equals(defaultColor.value));
},
);
});
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final pathway = Pathway.straight(
position: position,
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
group('fixtures', () {
flameTester.test(
'has only one ChainShape when singleWall is true',
(game) async {
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
singleWall: true,
);
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 1);
final fixture = pathway.body.fixtures[0];
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
},
);
flameTester.test(
'has two ChainShape when singleWall is false (default)',
(game) async {
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 2);
for (final fixture in pathway.body.fixtures) {
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
}
},
);
});
});
group('arc', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final pathway = Pathway.arc(
position: position,
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('bezier curve', () {
final controlPoints = [
Vector2(0, 0),
Vector2(50, 0),
Vector2(0, 50),
Vector2(50, 50),
];
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final pathway = Pathway.bezierCurve(
position: position,
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
});
}
Loading…
Cancel
Save