feat: adds ball turbocharge effect (#66)

* feat: adding ball boost effect

* fix: lint
pull/100/head
Erick 4 years ago committed by GitHub
parent e6dca1ed7f
commit de963cbc86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:ui';
import 'package:flame/components.dart';
@ -27,6 +28,9 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
/// The base [Color] used to tint this [Ball]
final Color baseColor;
double _boostTimer = 0;
static const _boostDuration = 2.0;
@override
Future<void> onLoad() async {
await super.onLoad();
@ -69,4 +73,26 @@ class Ball<T extends Forge2DGame> extends BodyComponent<T>
void resume() {
body.setType(BodyType.dynamic);
}
@override
void update(double dt) {
super.update(dt);
if (_boostTimer > 0) {
_boostTimer -= dt;
final direction = body.linearVelocity.normalized();
final effect = FireEffect(
burstPower: _boostTimer,
direction: direction,
position: body.position,
);
unawaited(gameRef.add(effect));
}
}
/// Applies a boost on this [Ball]
void boost(Vector2 impulse) {
body.applyLinearImpulse(impulse);
_boostTimer = _boostDuration;
}
}

@ -1,3 +1,4 @@
export 'ball.dart';
export 'fire_effect.dart';
export 'initial_position.dart';
export 'layer.dart';

@ -0,0 +1,113 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:flame/particles.dart';
import 'package:flame_forge2d/flame_forge2d.dart' hide Particle;
import 'package:flutter/material.dart';
const _particleRadius = 0.25;
// TODO(erickzanardo): This component could just be a ParticleComponet,
/// unfortunately there is a Particle Component is not a PositionComponent,
/// which makes it hard to be used since we have camera transformations and on
// top of that, PositionComponent has a bug inside forge 2d games
///
/// https://github.com/flame-engine/flame/issues/1484
/// https://github.com/flame-engine/flame/issues/1484
/// {@template fire_effect}
/// A [BodyComponent] which creates a fire trail effect using the given
/// parameters
/// {@endtemplate}
class FireEffect extends BodyComponent {
/// {@macro fire_effect}
FireEffect({
required this.burstPower,
required this.position,
required this.direction,
});
/// A [double] value that will define how "strong" the burst of particles
/// will be
final double burstPower;
/// The position of the burst
final Vector2 position;
/// Which direction the burst will aim
final Vector2 direction;
late Particle _particle;
@override
Body createBody() {
final bodyDef = BodyDef()..position = position;
final fixtureDef = FixtureDef(CircleShape()..radius = 0)..isSensor = true;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
Future<void> onLoad() async {
await super.onLoad();
final children = [
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.yellow.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.red.darken((index + 1) / 4),
);
}),
...List.generate(4, (index) {
return CircleParticle(
radius: _particleRadius,
paint: Paint()..color = Colors.orange.darken((index + 1) / 4),
);
}),
];
final rng = math.Random();
final spreadTween = Tween<double>(begin: -0.2, end: 0.2);
_particle = Particle.generate(
count: (rng.nextDouble() * (burstPower * 10)).toInt(),
generator: (_) {
final spread = Vector2(
spreadTween.transform(rng.nextDouble()),
spreadTween.transform(rng.nextDouble()),
);
final finalDirection = Vector2(direction.x, -direction.y) + spread;
final speed = finalDirection * (burstPower * 20);
return AcceleratedParticle(
lifespan: 5 / burstPower,
position: Vector2.zero(),
speed: speed,
child: children[rng.nextInt(children.length)],
);
},
);
}
@override
void update(double dt) {
super.update(dt);
_particle.update(dt);
if (_particle.shouldRemove) {
removeFromParent();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
_particle.render(canvas);
}
}

@ -1,11 +1,2 @@
import 'package:flame_forge2d/flame_forge2d.dart';
String buildSourceLink(String path) {
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
}
class BasicGame extends Forge2DGame {
BasicGame() {
images.prefix = '';
}
}
export 'games.dart';
export 'methods.dart';

@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
class BasicGame extends Forge2DGame {
BasicGame() {
images.prefix = '';
}
}
abstract class LineGame extends BasicGame with PanDetector {
Vector2? _lineEnd;
@override
Future<void> onLoad() async {
await super.onLoad();
camera.followVector2(Vector2.zero());
unawaited(add(_PreviewLine()));
}
@override
void onPanStart(DragStartInfo info) {
_lineEnd = info.eventPosition.game;
}
@override
void onPanUpdate(DragUpdateInfo info) {
_lineEnd = info.eventPosition.game;
}
@override
void onPanEnd(DragEndInfo info) {
if (_lineEnd != null) {
final line = _lineEnd! - Vector2.zero();
onLine(line);
_lineEnd = null;
}
}
void onLine(Vector2 line);
}
class _PreviewLine extends PositionComponent with HasGameRef<LineGame> {
static final _previewLinePaint = Paint()
..color = Colors.pink
..strokeWidth = 0.2
..style = PaintingStyle.stroke;
Vector2? lineEnd;
@override
void update(double dt) {
super.update(dt);
lineEnd = gameRef._lineEnd?.clone()?..multiply(Vector2(1, -1));
}
@override
void render(Canvas canvas) {
super.render(canvas);
if (lineEnd != null) {
canvas.drawLine(
Vector2.zero().toOffset(),
lineEnd!.toOffset(),
_previewLinePaint,
);
}
}
}

@ -0,0 +1,3 @@
String buildSourceLink(String path) {
return 'https://github.com/VGVentures/pinball/tree/main/packages/pinball_components/sandbox/lib/stories/$path';
}

@ -6,11 +6,13 @@
// https://opensource.org/licenses/MIT.
import 'package:dashbook/dashbook.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/stories/effects/effects.dart';
import 'package:sandbox/stories/stories.dart';
void main() {
final dashbook = Dashbook(theme: ThemeData.dark());
addBallStories(dashbook);
addEffectsStories(dashbook);
runApp(dashbook);
}

@ -2,10 +2,12 @@ import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/ball/ball_booster.dart';
import 'package:sandbox/stories/ball/basic.dart';
void addBallStories(Dashbook dashbook) {
dashbook.storiesOf('Ball').add(
dashbook.storiesOf('Ball')
..add(
'Basic',
(context) => GameWidget(
game: BasicBallGame(
@ -14,5 +16,13 @@ void addBallStories(Dashbook dashbook) {
),
codeLink: buildSourceLink('ball/basic.dart'),
info: BasicBallGame.info,
)
..add(
'Booster',
(context) => GameWidget(
game: BallBoosterExample(),
),
codeLink: buildSourceLink('ball/ball_booster.dart'),
info: BallBoosterExample.info,
);
}

@ -0,0 +1,16 @@
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BallBoosterExample extends LineGame {
static const info = '';
@override
void onLine(Vector2 line) {
final ball = Ball(baseColor: Colors.transparent);
add(ball);
ball.mounted.then((value) => ball.boost(line * -1 * 20));
}
}

@ -4,7 +4,7 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class BasicBallGame extends BasicGame with TapDetector {
BasicBallGame({ required this.color });
BasicBallGame({required this.color});
static const info = '''
Basic example of how a Ball works, tap anywhere on the
@ -15,8 +15,8 @@ class BasicBallGame extends BasicGame with TapDetector {
@override
void onTapUp(TapUpInfo info) {
add(Ball(baseColor: color)
..initialPosition = info.eventPosition.game,
add(
Ball(baseColor: color)..initialPosition = info.eventPosition.game,
);
}
}

@ -0,0 +1,13 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';
import 'package:sandbox/common/common.dart';
import 'package:sandbox/stories/effects/fire_effect.dart';
void addEffectsStories(Dashbook dashbook) {
dashbook.storiesOf('Effects').add(
'Fire Effect',
(context) => GameWidget(game: FireEffectExample()),
codeLink: buildSourceLink('effects/fire_effect.dart'),
info: FireEffectExample.info,
);
}

@ -0,0 +1,46 @@
import 'package:flame/components.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:sandbox/common/common.dart';
class FireEffectExample extends LineGame {
static const info = 'Demonstrate the fire trail effect '
'drag a line to define the trail direction';
@override
void onLine(Vector2 line) {
add(_EffectEmitter(line));
}
}
class _EffectEmitter extends Component {
_EffectEmitter(this.line) {
_direction = line.normalized();
_force = line.length;
}
static const _timerLimit = 2.0;
var _timer = _timerLimit;
final Vector2 line;
late Vector2 _direction;
late double _force;
@override
void update(double dt) {
super.update(dt);
if (_timer > 0) {
add(
FireEffect(
burstPower: (_timer / _timerLimit) * _force,
position: Vector2.zero(),
direction: _direction,
),
);
_timer -= dt;
} else {
removeFromParent();
}
}
}

@ -1 +1,2 @@
export 'mocks.dart';
export 'test_game.dart';

@ -0,0 +1,5 @@
import 'dart:ui';
import 'package:mocktail/mocktail.dart';
class MockCanvas extends Mock implements Canvas {}

@ -158,5 +158,29 @@ void main() {
);
});
});
group('boost', () {
flameTester.test('applies an impulse to the ball', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
expect(ball.body.linearVelocity, equals(Vector2.zero()));
ball.boost(Vector2.all(10));
expect(ball.body.linearVelocity.x, greaterThan(0));
expect(ball.body.linearVelocity.y, greaterThan(0));
});
flameTester.test('adds fire effect components to the game', (game) async {
final ball = Ball(baseColor: Colors.blue);
await game.ensureAdd(ball);
ball.boost(Vector2.all(10));
game.update(0);
await game.ready();
expect(game.children.whereType<FireEffect>().length, greaterThan(0));
});
});
});
}

@ -0,0 +1,55 @@
// ignore_for_file: cascade_invocations
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(TestGame.new);
setUpAll(() {
registerFallbackValue(Offset.zero);
registerFallbackValue(Paint());
});
group('FireEffect', () {
flameTester.test('is removed once its particles are done', (game) async {
await game.ensureAdd(
FireEffect(
burstPower: 1,
position: Vector2.zero(),
direction: Vector2.all(2),
),
);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(1));
game.update(5);
await game.ready();
expect(game.children.whereType<FireEffect>().length, equals(0));
});
flameTester.test('render circles on the canvas', (game) async {
final effect = FireEffect(
burstPower: 1,
position: Vector2.zero(),
direction: Vector2.all(2),
);
await game.ensureAdd(effect);
await game.ready();
final canvas = MockCanvas();
effect.render(canvas);
verify(() => canvas.drawCircle(any(), any(), any()))
.called(greaterThan(0));
});
});
}
Loading…
Cancel
Save