feat: Blueprints as a method of grouping game components

pull/62/head
Erick Zanardo 4 years ago
parent 4c0774b792
commit 27324bd222

@ -0,0 +1,88 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_forge2d/contact_callbacks.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
void attach(FlameGame game) {
build();
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
void attach(FlameGame game) {
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)`
void addFromBlueprint(Blueprint blueprint) {
blueprint.attach(this);
}
}

@ -5,12 +5,37 @@ import 'dart:math';
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/extensions.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
// TODO(erickzanardo): change this to use the layer class // TODO(erickzanardo): change this to use the layer class
// that will be introduced on the path PR // that will be introduced on the path PR
const _spaceShipBits = 0x0002; 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 size = 20.0;
@override
void build() {
final position = Vector2(20, -24);
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,
]);
addAllContactCallback([
SpaceshipHoleBallContactCallback(),
SpaceshipEntranceBallContactCallback(),
]);
}
}
/// {@template spaceship_saucer} /// {@template spaceship_saucer}
/// A [BodyComponent] for the base, or the saucer of the spaceship /// A [BodyComponent] for the base, or the saucer of the spaceship
@ -36,7 +61,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add( await add(
SpriteComponent( SpriteComponent(
sprite: sprites.first, sprite: sprites.first,
size: Vector2.all(_spaceShipSize), size: Vector2.all(Spaceship.size),
anchor: Anchor.center, anchor: Anchor.center,
), ),
); );
@ -44,9 +69,9 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
await add( await add(
SpriteComponent( SpriteComponent(
sprite: sprites.last, sprite: sprites.last,
size: Vector2(_spaceShipSize + 0.5, _spaceShipSize / 2), size: Vector2(Spaceship.size + 0.5, Spaceship.size / 2),
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -(_spaceShipSize / 3.5)), position: Vector2(0, -(Spaceship.size / 3.5)),
), ),
); );
@ -55,7 +80,7 @@ class SpaceshipSaucer extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 2; final circleShape = CircleShape()..radius = Spaceship.size / 2;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
@ -92,7 +117,7 @@ class SpaceshipBridgeTop extends BodyComponent with InitialPosition {
SpriteComponent( SpriteComponent(
sprite: sprite, sprite: sprite,
anchor: Anchor.center, anchor: Anchor.center,
size: Vector2(_spaceShipSize / 2.5 - 1, _spaceShipSize / 5), size: Vector2(Spaceship.size / 2.5 - 1, Spaceship.size / 5),
), ),
); );
} }
@ -134,7 +159,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
stepTime: 0.2, stepTime: 0.2,
textureSize: Vector2(160, 114), textureSize: Vector2(160, 114),
), ),
size: Vector2.all(_spaceShipSize / 2.5), size: Vector2.all(Spaceship.size / 2.5),
anchor: Anchor.center, anchor: Anchor.center,
), ),
); );
@ -142,7 +167,7 @@ class SpaceshipBridge extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
final circleShape = CircleShape()..radius = _spaceShipSize / 5; final circleShape = CircleShape()..radius = Spaceship.size / 5;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
@ -171,7 +196,7 @@ class SpaceshipEntrance extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
const radius = _spaceShipSize / 2; const radius = Spaceship.size / 2;
final entranceShape = PolygonShape() final entranceShape = PolygonShape()
..setAsEdge( ..setAsEdge(
Vector2( Vector2(
@ -208,7 +233,7 @@ class SpaceshipHole extends BodyComponent with InitialPosition {
@override @override
Body createBody() { Body createBody() {
renderBody = false; renderBody = false;
final circleShape = CircleShape()..radius = _spaceShipSize / 80; final circleShape = CircleShape()..radius = Spaceship.size / 80;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
@ -247,9 +272,9 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
await add( await add(
SpriteComponent( SpriteComponent(
sprite: sprite, sprite: sprite,
size: Vector2(_spaceShipSize, (_spaceShipSize / 2) + 1), size: Vector2(Spaceship.size, (Spaceship.size / 2) + 1),
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(-_spaceShipSize / 4, 0), position: Vector2(-Spaceship.size / 4, 0),
angle: 90 * pi / 180, angle: 90 * pi / 180,
), ),
); );
@ -259,7 +284,7 @@ class SpaceshipWall extends BodyComponent with InitialPosition {
Body createBody() { Body createBody() {
renderBody = false; renderBody = false;
const radius = _spaceShipSize / 2; const radius = Spaceship.size / 2;
final wallShape = ChainShape() final wallShape = ChainShape()
..createChain( ..createChain(

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/extensions.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
@ -29,7 +30,7 @@ class PinballGame extends Forge2DGame
unawaited(_addPlunger()); unawaited(_addPlunger());
unawaited(_addPaths()); unawaited(_addPaths());
unawaited(_addSpaceship()); addFromBlueprint(Spaceship());
// Corner wall above plunger so the ball deflects into the rest of the // Corner wall above plunger so the ball deflects into the rest of the
// board. // 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() { void spawnBall() {
final ball = Ball(); final ball = Ball();
add( add(
@ -107,8 +93,6 @@ class PinballGame extends Forge2DGame
addContactCallback(BallScorePointsCallback()); addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
addContactCallback(BonusLetterBallContactCallback()); addContactCallback(BonusLetterBallContactCallback());
addContactCallback(SpaceshipHoleBallContactCallback());
addContactCallback(SpaceshipEntranceBallContactCallback());
} }
Future<void> _addGameBoundaries() async { Future<void> _addGameBoundaries() async {

@ -0,0 +1,101 @@
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/extensions.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',
() {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
final blueprint = MyBlueprint()..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', () {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
MyForge2dBlueprint().attach(mockGame);
verify(() => mockGame.addContactCallback(any())).called(3);
});
test(
'throws assertion error when adding to an already attached blueprint',
() {
final mockGame = MockPinballGame();
when(() => mockGame.addAll(any())).thenAnswer((_) async {});
when(() => mockGame.addContactCallback(any())).thenAnswer((_) async {});
final blueprint = MyForge2dBlueprint()..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,
);
});
});
}

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

Loading…
Cancel
Save