feat: defined AlienBumper behaviours

pull/234/head
alestiago 3 years ago
parent 318da8c23a
commit 1420f33049

@ -0,0 +1,36 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class AlienBumperContactBehavior extends Component with ContactCallbacks {
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is! AlienBumper) return;
final userData = parent.body.userData;
if (userData is ContactCallbacksNotifer) {
userData.addCallback(this);
} else {
parent.body.userData = this;
}
}
@override
void beginContact(Object other, Contact contact) {
super.beginContact(other, contact);
if (other is! Ball) return;
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is! AlienBumper) return;
parent.state = AlienBumperState.inactive;
}
}

@ -0,0 +1,51 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
/// {@template alien_bumper_sprite_behavior}
///
/// {@endtemplate}
class AlienBumperSpriteBehavior extends TimerComponent {
/// {@macro alien_bumper_sprite_behavior}
AlienBumperSpriteBehavior()
: super(
period: 0.05,
removeOnFinish: false,
);
void _onNewState(AlienBumperState state) {
switch (state) {
case AlienBumperState.active:
timer.stop();
break;
case AlienBumperState.inactive:
timer
..reset()
..start();
break;
}
}
@override
Future<void> onLoad() async {
await super.onLoad();
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is! AlienBumper) return;
timer.stop();
parent.stream.listen(_onNewState);
}
@override
void onTick() {
super.onTick();
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is! AlienBumper) return;
parent.state = AlienBumperState.active;
}
}

@ -0,0 +1,2 @@
export 'alien_bumper_contact_behavior.dart';
export 'alien_bumper_sprite_behavior.dart';

@ -1,7 +1,7 @@
// ignore_for_file: avoid_renaming_method_parameters // ignore_for_file: avoid_renaming_method_parameters
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/alien_zone/alien_bumper/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart'; import 'package:pinball_flame/pinball_flame.dart';
@ -20,28 +20,17 @@ class AlienZone extends Blueprint {
AlienBumper.a( AlienBumper.a(
children: [ children: [
ScoringBehaviour(points: 20), ScoringBehaviour(points: 20),
AlienBumperContactBehavior(),
AlienBumperSpriteBehavior(),
], ],
)..initialPosition = Vector2(-32.52, -9.1), )..initialPosition = Vector2(-32.52, -9.1),
AlienBumper.b( AlienBumper.b(
children: [ children: [
ScoringBehaviour(points: 20), ScoringBehaviour(points: 20),
AlienBumperContactBehavior(),
AlienBumperSpriteBehavior(),
], ],
)..initialPosition = Vector2(-22.89, -17.35), )..initialPosition = Vector2(-22.89, -17.35),
], ],
); );
} }
/// Listens when a [Ball] bounces against an [AlienBumper].
// TODO(alestiago): Add Bumper animation behaviour.
// @visibleForTesting
// class AlienBumperBallContactCallback
// extends ContactCallback<AlienBumper, Ball> {
// @override
// void begin(
// AlienBumper alienBumper,
// Ball _,
// Contact __,
// ) {
// alienBumper.animate();
// }
// }

@ -1,4 +1,4 @@
export 'alien_zone.dart'; export 'alien_zone/alien_zone.dart';
export 'board.dart'; export 'board.dart';
export 'camera_controller.dart'; export 'camera_controller.dart';
export 'controlled_ball.dart'; export 'controlled_ball.dart';

@ -10,7 +10,7 @@ import 'package:pinball_flame/pinball_flame.dart';
/// ///
/// {@endtemplate} /// {@endtemplate}
class ScoringBehaviour extends Component class ScoringBehaviour extends Component
with ContactCallbacks, HasGameRef<PinballGame> { with ContactCallbacksNotifer, HasGameRef<PinballGame> {
/// {@macro scoring_behaviour} /// {@macro scoring_behaviour}
ScoringBehaviour({ ScoringBehaviour({
required int points, required int points,
@ -28,8 +28,8 @@ class ScoringBehaviour extends Component
if (parent is! BodyComponent) return; if (parent is! BodyComponent) return;
final userData = parent.body.userData; final userData = parent.body.userData;
if (userData is ContactCallbacks) { if (userData is ContactCallbacksNotifer) {
userData.add(this); userData.addCallback(this);
} else { } else {
parent.body.userData = this; parent.body.userData = this;
} }
@ -42,7 +42,6 @@ class ScoringBehaviour extends Component
gameRef.read<GameBloc>().add(Scored(points: _points)); gameRef.read<GameBloc>().add(Scored(points: _points));
gameRef.audio.score(); gameRef.audio.score();
gameRef.add( gameRef.add(
ScoreText( ScoreText(
text: _points.toString(), text: _points.toString(),

@ -53,7 +53,8 @@ class PinballGame extends Forge2DGame
final launcher = Launcher(); final launcher = Launcher();
unawaited(addFromBlueprint(launcher)); unawaited(addFromBlueprint(launcher));
unawaited(add(Board())); unawaited(add(Board()));
unawaited(add(AlienZone())); await addFromBlueprint(AlienZone());
await addFromBlueprint(SparkyFireZone()); await addFromBlueprint(SparkyFireZone());
unawaited(addFromBlueprint(Slingshots())); unawaited(addFromBlueprint(Slingshots()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));

@ -2,13 +2,39 @@ import 'dart:async';
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:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart'; import 'package:pinball_components/pinball_components.dart';
abstract class State<T> {
// TODO(alestiago): Investigate approaches to avoid having this as late.
late T _state;
T get state => _state;
set state(T value) {
if (value == _state) return;
_state = value;
_stream.sink.add(value);
}
final StreamController<T> _stream = StreamController<T>.broadcast();
Stream<T> get stream => _stream.stream;
}
/// Indicates the [AlienBumper]'s current sprite state.
enum AlienBumperState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
}
/// {@template alien_bumper} /// {@template alien_bumper}
/// Bumper for area under the [Spaceship]. /// Bumper for area under the [Spaceship].
/// {@endtemplate} /// {@endtemplate}
class AlienBumper extends BodyComponent with InitialPosition { class AlienBumper extends BodyComponent
with InitialPosition, State<AlienBumperState> {
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper._({ AlienBumper._({
required double majorRadius, required double majorRadius,
@ -21,14 +47,17 @@ class AlienBumper extends BodyComponent with InitialPosition {
super( super(
priority: RenderPriority.alienBumper, priority: RenderPriority.alienBumper,
children: [ children: [
_AlienBumperSpriteGroupComponent(
onAssetPath: onAssetPath,
offAssetPath: offAssetPath,
),
if (children != null) ...children, if (children != null) ...children,
], ],
renderBody: false, renderBody: false,
); ) {
_state = AlienBumperState.active;
_spriteGroupComponent = _AlienBumperSpriteGroupComponent(
offAssetPath: offAssetPath,
onAssetPath: onAssetPath,
state: state,
);
}
/// {@macro alien_bumper} /// {@macro alien_bumper}
AlienBumper.a({ AlienBumper.a({
@ -53,8 +82,17 @@ class AlienBumper extends BodyComponent with InitialPosition {
); );
final double _majorRadius; final double _majorRadius;
final double _minorRadius; final double _minorRadius;
late final _AlienBumperSpriteGroupComponent _spriteGroupComponent;
@override
Future<void> onLoad() async {
await super.onLoad();
await add(_spriteGroupComponent);
}
@override @override
Body createBody() { Body createBody() {
final shape = EllipseShape( final shape = EllipseShape(
@ -68,41 +106,24 @@ class AlienBumper extends BodyComponent with InitialPosition {
); );
final bodyDef = BodyDef( final bodyDef = BodyDef(
position: initialPosition, position: initialPosition,
userData: this,
); );
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
/// Animates the [AlienBumper].
Future<void> animate() async {
final spriteGroupComponent = firstChild<_AlienBumperSpriteGroupComponent>()
?..current = AlienBumperSpriteState.inactive;
await Future<void>.delayed(const Duration(milliseconds: 50));
spriteGroupComponent?.current = AlienBumperSpriteState.active;
}
}
/// Indicates the [AlienBumper]'s current sprite state.
@visibleForTesting
enum AlienBumperSpriteState {
/// A lit up bumper.
active,
/// A dimmed bumper.
inactive,
} }
class _AlienBumperSpriteGroupComponent class _AlienBumperSpriteGroupComponent
extends SpriteGroupComponent<AlienBumperSpriteState> with HasGameRef { extends SpriteGroupComponent<AlienBumperState> with HasGameRef {
_AlienBumperSpriteGroupComponent({ _AlienBumperSpriteGroupComponent({
required String onAssetPath, required String onAssetPath,
required String offAssetPath, required String offAssetPath,
required AlienBumperState state,
}) : _onAssetPath = onAssetPath, }) : _onAssetPath = onAssetPath,
_offAssetPath = offAssetPath, _offAssetPath = offAssetPath,
super( super(
anchor: Anchor.center, anchor: Anchor.center,
position: Vector2(0, -0.1), position: Vector2(0, -0.1),
current: state,
); );
final String _onAssetPath; final String _onAssetPath;
@ -113,14 +134,20 @@ class _AlienBumperSpriteGroupComponent
await super.onLoad(); await super.onLoad();
final sprites = { final sprites = {
AlienBumperSpriteState.active: AlienBumperState.active: Sprite(
Sprite(gameRef.images.fromCache(_onAssetPath)), gameRef.images.fromCache(_onAssetPath),
AlienBumperSpriteState.inactive: ),
AlienBumperState.inactive:
Sprite(gameRef.images.fromCache(_offAssetPath)), Sprite(gameRef.images.fromCache(_offAssetPath)),
}; };
this.sprites = sprites; this.sprites = sprites;
current = AlienBumperSpriteState.active;
size = sprites[current]!.originalSize / 10; size = sprites[current]!.originalSize / 10;
// TODO(alestiago): Refactor once the following is merged:
// https://github.com/flame-engine/flame/pull/1566
final parent = this.parent;
if (parent is! AlienBumper) return;
parent.stream.listen((state) => current = state);
} }
} }

@ -40,21 +40,17 @@ void main() {
expect( expect(
spriteGroupComponent.current, spriteGroupComponent.current,
equals(AlienBumperSpriteState.active), equals(AlienBumperState.active),
); );
final future = bumper.animate();
expect( expect(
spriteGroupComponent.current, spriteGroupComponent.current,
equals(AlienBumperSpriteState.inactive), equals(AlienBumperState.inactive),
); );
await future;
expect( expect(
spriteGroupComponent.current, spriteGroupComponent.current,
equals(AlienBumperSpriteState.active), equals(AlienBumperState.active),
); );
}); });
}); });

@ -1,38 +1,60 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
/// {@template contact_callbacks_adder} class ContactCallbacksNotifer implements ContactCallbacks {
/// final List<ContactCallbacks> _callbacks = [];
/// {@endtemplate}
// TODO(alestiago): Consider adding streams to [ContactCallbacks]. @override
extension ContactCallbacksAdder on ContactCallbacks { @mustCallSuper
/// {@macro contact_callbacks_adder} void beginContact(Object other, Contact contact) {
void add(ContactCallbacks contactCallbacks) { onBeginContact?.call(other, contact);
if (contactCallbacks.onBeginContact != null) { for (final callback in _callbacks) {
onBeginContact = (other, contact) { callback.beginContact(other, contact);
onBeginContact?.call(other, contact);
contactCallbacks.beginContact(other, contact);
};
} }
}
if (contactCallbacks.onEndContact != null) { @override
onEndContact = (other, contact) { @mustCallSuper
onEndContact?.call(other, contact); void endContact(Object other, Contact contact) {
contactCallbacks.endContact(other, contact); onEndContact?.call(other, contact);
}; for (final callback in _callbacks) {
callback.endContact(other, contact);
} }
}
if (contactCallbacks.onPreSolve != null) { @override
onPreSolve = (other, contact, oldManifold) { @mustCallSuper
onPreSolve?.call(other, contact, oldManifold); void preSolve(Object other, Contact contact, Manifold oldManifold) {
contactCallbacks.preSolve(other, contact, oldManifold); onPreSolve?.call(other, contact, oldManifold);
}; for (final callback in _callbacks) {
callback.preSolve(other, contact, oldManifold);
} }
}
if (contactCallbacks.onPostSolve != null) { @override
onPostSolve = (other, contact, impulse) { @mustCallSuper
onPostSolve?.call(other, contact, impulse); void postSolve(Object other, Contact contact, ContactImpulse impulse) {
contactCallbacks.postSolve(other, contact, impulse); onPostSolve?.call(other, contact, impulse);
}; for (final callback in _callbacks) {
callback.postSolve(other, contact, impulse);
} }
} }
void addCallback(ContactCallbacks callback) {
_callbacks.add(callback);
}
@override
void Function(Object other, Contact contact)? onBeginContact;
@override
void Function(Object other, Contact contact)? onEndContact;
@override
void Function(Object other, Contact contact, ContactImpulse impulse)?
onPostSolve;
@override
void Function(Object other, Contact contact, Manifold oldManifold)?
onPreSolve;
} }

Loading…
Cancel
Save