feat: Blueprints as a method of grouping game components (#62)

* feat: Blueprints as a method of grouping game components

* fix: lint

* feat: pr suggestions

* feat: pr suggestions

* fix: test from merge
pull/69/head
Erick 2 years ago committed by GitHub
parent 74e448cbf9
commit 1af0c47328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,87 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
const _attachedErrorMessage = "Can't add to attached Blueprints";
// TODO(erickzanardo): Keeping this inside our code base
// so we can experiment with the idea, but this is a
// potential upstream change on Flame.
/// A [Blueprint] is a virtual way of grouping [Component]s
/// that are related, but they need to be added directly on
/// the [FlameGame] level.
abstract class Blueprint {
final List<Component> _components = [];
bool _isAttached = false;
/// Called before the the [Component]s managed
/// by this blueprint is added to the [FlameGame]
void build();
/// Attach the [Component]s built on [build] to the [game]
/// instance
@mustCallSuper
Future<void> attach(FlameGame game) async {
build();
await game.addAll(_components);
_isAttached = true;
}
/// Adds a list of [Component]s to this blueprint.
void addAll(List<Component> components) {
assert(!_isAttached, _attachedErrorMessage);
_components.addAll(components);
}
/// Adds a single [Component] to this blueprint.
void add(Component component) {
assert(!_isAttached, _attachedErrorMessage);
_components.add(component);
}
/// Returns a copy of the components built by this blueprint
List<Component> get components => List.unmodifiable(_components);
}
/// A [Blueprint] that provides additional
/// structures specific to flame_forge2d
abstract class Forge2DBlueprint extends Blueprint {
final List<ContactCallback> _callbacks = [];
/// Adds a single [ContactCallback] to this blueprint
void addContactCallback(ContactCallback callback) {
assert(!_isAttached, _attachedErrorMessage);
_callbacks.add(callback);
}
/// Adds a collection of [ContactCallback]s to this blueprint
void addAllContactCallback(List<ContactCallback> callbacks) {
assert(!_isAttached, _attachedErrorMessage);
_callbacks.addAll(callbacks);
}
@override
Future<void> attach(FlameGame game) async {
await super.attach(game);
assert(game is Forge2DGame, 'Forge2DBlueprint used outside a Forge2DGame');
for (final callback in _callbacks) {
(game as Forge2DGame).addContactCallback(callback);
}
}
/// Returns a copy of the callbacks built by this blueprint
List<ContactCallback> get callbacks => List.unmodifiable(_callbacks);
}
/// Adds helper methods regardin [Blueprint]s to [FlameGame]
extension FlameGameBlueprint on FlameGame {
/// Shortcut to attach a [Blueprint] instance to this game
/// equivalent to `MyBluepinrt().attach(game)`
Future<void> addFromBlueprint(Blueprint blueprint) async {
await blueprint.attach(this);
}
}

@ -5,12 +5,38 @@ import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
// TODO(erickzanardo): change this to use the layer class
// that will be introduced on the path PR
const _spaceShipBits = 0x0002;
const _spaceShipSize = 20.0;
/// A [Blueprint] which creates the spaceship feature.
class Spaceship extends Forge2DBlueprint {
/// Total size of the spaceship
static const radius = 10.0;
@override
void build() {
final position = Vector2(20, -24);
addAllContactCallback([
SpaceshipHoleBallContactCallback(),
SpaceshipEntranceBallContactCallback(),
]);
addAll([
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5),
SpaceshipHole()..initialPosition = position - Vector2(5, 4),
SpaceshipHole()..initialPosition = position - Vector2(-5, 4),
SpaceshipWall()..initialPosition = position,
]);
}
}
/// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship
@ -36,7 +62,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprites.first,
size: Vector2.all(_spaceShipSize),
size: Vector2.all(Spaceship.radius * 2),
anchor: Anchor.center,
),
);
@ -44,9 +70,9 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprites.last,
size: Vector2(_spaceShipSize + 0.5, _spaceShipSize / 2),
size: Vector2((Spaceship.radius * 2) + 0.5, Spaceship.radius),
anchor: Anchor.center,
position: Vector2(0, -(_spaceShipSize / 3.5)),
position: Vector2(0, -((Spaceship.radius * 2) / 3.5)),
),
);
@ -55,7 +81,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 2;
final circleShape = CircleShape()..radius = Spaceship.radius;
final bodyDef = BodyDef()
..userData = this
@ -92,7 +118,7 @@ class SpaceshipBridgeTop extends BodyComponent with InitialPosition {
SpriteComponent(
sprite: sprite,
anchor: Anchor.center,
size: Vector2(_spaceShipSize / 2.5 - 1, _spaceShipSize / 5),
size: Vector2((Spaceship.radius * 2) / 2.5 - 1, Spaceship.radius / 2.5),
),
);
}
@ -134,7 +160,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
stepTime: 0.2,
textureSize: Vector2(160, 114),
),
size: Vector2.all(_spaceShipSize / 2.5),
size: Vector2.all((Spaceship.radius * 2) / 2.5),
anchor: Anchor.center,
),
);
@ -142,7 +168,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
@override
Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 5;
final circleShape = CircleShape()..radius = Spaceship.radius / 2.5;
final bodyDef = BodyDef()
..userData = this
@ -171,16 +197,15 @@ class SpaceshipEntrance extends BodyComponent with InitialPosition {
@override
Body createBody() {
const radius = _spaceShipSize / 2;
final entranceShape = PolygonShape()
..setAsEdge(
Vector2(
radius * cos(20 * pi / 180),
radius * sin(20 * pi / 180),
Spaceship.radius * cos(20 * pi / 180),
Spaceship.radius * sin(20 * pi / 180),
),
Vector2(
radius * cos(340 * pi / 180),
radius * sin(340 * pi / 180),
Spaceship.radius * cos(340 * pi / 180),
Spaceship.radius * sin(340 * pi / 180),
),
);
@ -208,7 +233,7 @@ class SpaceshipHole extends BodyComponent with InitialPosition {
@override
Body createBody() {
renderBody = false;
final circleShape = CircleShape()..radius = _spaceShipSize / 80;
final circleShape = CircleShape()..radius = Spaceship.radius / 40;
final bodyDef = BodyDef()
..userData = this
@ -247,9 +272,9 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
await add(
SpriteComponent(
sprite: sprite,
size: Vector2(_spaceShipSize, (_spaceShipSize / 2) + 1),
size: Vector2(Spaceship.radius * 2, Spaceship.radius + 1),
anchor: Anchor.center,
position: Vector2(-_spaceShipSize / 4, 0),
position: Vector2(-Spaceship.radius / 2, 0),
angle: 90 * pi / 180,
),
);
@ -259,15 +284,13 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
Body createBody() {
renderBody = false;
const radius = _spaceShipSize / 2;
final wallShape = ChainShape()
..createChain(
[
for (var angle = 20; angle <= 340; angle++)
Vector2(
radius * cos(angle * pi / 180),
radius * sin(angle * pi / 180),
Spaceship.radius * cos(angle * pi / 180),
Spaceship.radius * sin(angle * pi / 180),
),
],
);

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
@ -29,7 +30,7 @@ class PinballGame extends Forge2DGame
unawaited(_addPlunger());
unawaited(_addPaths());
unawaited(_addSpaceship());
unawaited(addFromBlueprint(Spaceship()));
// Corner wall above plunger so the ball deflects into the rest of the
// board.
@ -80,21 +81,6 @@ class PinballGame extends Forge2DGame
);
}
Future<void> _addSpaceship() async {
final position = Vector2(20, -24);
await addAll(
[
SpaceshipSaucer()..initialPosition = position,
SpaceshipEntrance()..initialPosition = position,
SpaceshipBridge()..initialPosition = position,
SpaceshipBridgeTop()..initialPosition = position + Vector2(0, 5.5),
SpaceshipHole()..initialPosition = position - Vector2(5, 4),
SpaceshipHole()..initialPosition = position - Vector2(-5, 4),
SpaceshipWall()..initialPosition = position,
],
);
}
void spawnBall() {
final ball = Ball();
add(
@ -107,8 +93,6 @@ class PinballGame extends Forge2DGame
addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback());
addContactCallback(SpaceshipHoleBallContactCallback());
addContactCallback(SpaceshipEntranceBallContactCallback());
}
Future<void> _addGameBoundaries() async {

@ -0,0 +1,103 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/flame/blueprint.dart';
import 'package:pinball/game/game.dart';
import '../helpers/helpers.dart';
class MyBlueprint extends Blueprint {
@override
void build() {
add(Component());
addAll([Component(), Component()]);
}
}
class MyForge2dBlueprint extends Forge2DBlueprint {
@override
void build() {
addContactCallback(MockContactCallback());
addAllContactCallback([MockContactCallback(), MockContactCallback()]);
}
}
void main() {
group('Blueprint', () {
test('components can be added to it', () {
final blueprint = MyBlueprint()..build();
expect(blueprint.components.length, equals(3));
});
test('adds the components to a game on attach', () {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
MyBlueprint().attach(mockGame);
verify(() => mockGame.addAll(any())).called(1);
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint();
await blueprint.attach(mockGame);
expect(() => blueprint.add(Component()), throwsAssertionError);
expect(() => blueprint.addAll([Component()]), throwsAssertionError);
},
);
});
group('Forge2DBlueprint', () {
setUpAll(() {
registerFallbackValue(SpaceshipHoleBallContactCallback());
});
test('callbacks can be added to it', () {
final blueprint = MyForge2dBlueprint()..build();
expect(blueprint.callbacks.length, equals(3));
});
test('adds the callbacks to a game on attach', () async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
await MyForge2dBlueprint().attach(mockGame);
verify(() => mockGame.addContactCallback(any())).called(3);
});
test(
'throws assertion error when adding to an already attached blueprint',
() async {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint();
await blueprint.attach(mockGame);
expect(
() => blueprint.addContactCallback(MockContactCallback()),
throwsAssertionError,
);
expect(
() => blueprint.addAllContactCallback([MockContactCallback()]),
throwsAssertionError,
);
},
);
test('throws assertion error when used on a non Forge2dGame', () {
expect(
() => MyForge2dBlueprint().attach(FlameGame()),
throwsAssertionError,
);
});
});
}

@ -76,7 +76,7 @@ void main() {
(game) async {
final ball = Ball();
await game.ensureAdd(ball);
await ball.mounted;
await game.ready();
final fixture = ball.body.fixtures[0];
expect(fixture.filterData.maskBits, equals(Layer.board.maskBits));

@ -18,6 +18,9 @@ class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {}
class MockContactCallback extends Mock
implements ContactCallback<Object, Object> {}
class MockRampOpening extends Mock implements RampOpening {}
class MockRampOpeningBallContactCallback extends Mock

Loading…
Cancel
Save