From 20529c140695ca6e9a4b9558a0724ce6959fa7f7 Mon Sep 17 00:00:00 2001 From: arturplaczek <33895544+arturplaczek@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:41:05 +0200 Subject: [PATCH 1/2] chore: SpriteAnimationWidget from pre release flame (#228) --- packages/pinball_flame/lib/pinball_flame.dart | 1 + .../lib/src/sprite_animation.dart | 102 ++++++++++++++++++ .../test/src/sprite_animation_test.dart | 74 +++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 packages/pinball_flame/lib/src/sprite_animation.dart create mode 100644 packages/pinball_flame/test/src/sprite_animation_test.dart diff --git a/packages/pinball_flame/lib/pinball_flame.dart b/packages/pinball_flame/lib/pinball_flame.dart index 2d2e760b..709e7627 100644 --- a/packages/pinball_flame/lib/pinball_flame.dart +++ b/packages/pinball_flame/lib/pinball_flame.dart @@ -3,3 +3,4 @@ library pinball_flame; export 'src/blueprint.dart'; export 'src/component_controller.dart'; export 'src/keyboard_input_controller.dart'; +export 'src/sprite_animation.dart'; diff --git a/packages/pinball_flame/lib/src/sprite_animation.dart b/packages/pinball_flame/lib/src/sprite_animation.dart new file mode 100644 index 00000000..2990fb14 --- /dev/null +++ b/packages/pinball_flame/lib/src/sprite_animation.dart @@ -0,0 +1,102 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/image_composition.dart'; +import 'package:flutter/material.dart' hide Animation; + +/// {@template flame.widgets.sprite_animation_widget} +/// A [StatelessWidget] that renders a [SpriteAnimation]. +/// {@endtemplate} +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 +class SpriteAnimationWidget extends StatelessWidget { + /// {@macro flame.widgets.sprite_animation_widget} + const SpriteAnimationWidget({ + required this.controller, + this.anchor = Anchor.topLeft, + Key? key, + }) : super(key: key); + + /// The positioning [Anchor]. + final Anchor anchor; + + final SpriteAnimationController controller; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (_, __) { + return CustomPaint( + painter: SpritePainter( + controller.animation.getSprite(), + anchor, + ), + ); + }, + ); + } +} + +class SpriteAnimationController extends AnimationController { + SpriteAnimationController({ + required TickerProvider vsync, + required this.animation, + }) : super(vsync: vsync) { + duration = Duration(seconds: animation.totalDuration().ceil()); + } + + final SpriteAnimation animation; + + double? _lastUpdated; + + @override + void notifyListeners() { + super.notifyListeners(); + + final now = DateTime.now().millisecond.toDouble(); + final dt = max(0, (now - (_lastUpdated ?? 0)) / 1000); + animation.update(dt); + _lastUpdated = now; + } +} + +class SpritePainter extends CustomPainter { + SpritePainter( + this._sprite, + this._anchor, { + double angle = 0, + }) : _angle = angle; + + final Sprite _sprite; + final Anchor _anchor; + final double _angle; + + @override + bool shouldRepaint(SpritePainter oldDelegate) { + return oldDelegate._sprite != _sprite || + oldDelegate._anchor != _anchor || + oldDelegate._angle != _angle; + } + + @override + void paint(Canvas canvas, Size size) { + final boxSize = size.toVector2(); + final rate = boxSize.clone()..divide(_sprite.srcSize); + final minRate = min(rate.x, rate.y); + final paintSize = _sprite.srcSize * minRate; + final anchorPosition = _anchor.toVector2(); + final boxAnchorPosition = boxSize.clone()..multiply(anchorPosition); + final spriteAnchorPosition = anchorPosition..multiply(paintSize); + + canvas + ..translateVector(boxAnchorPosition..sub(spriteAnchorPosition)) + ..renderRotated( + _angle, + spriteAnchorPosition, + (canvas) => _sprite.render(canvas, size: paintSize), + ); + } +} diff --git a/packages/pinball_flame/test/src/sprite_animation_test.dart b/packages/pinball_flame/test/src/sprite_animation_test.dart new file mode 100644 index 00000000..e3b287de --- /dev/null +++ b/packages/pinball_flame/test/src/sprite_animation_test.dart @@ -0,0 +1,74 @@ +import 'package:flame/components.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +class MockSpriteAnimationController extends Mock + implements SpriteAnimationController {} + +class MockSpriteAnimation extends Mock implements SpriteAnimation {} + +class MockSprite extends Mock implements Sprite {} + +// TODO(arturplaczek): Remove when this PR will be merged. +// https://github.com/flame-engine/flame/pull/1552 + +void main() { + group('PinballSpriteAnimationWidget', () { + late SpriteAnimationController controller; + late SpriteAnimation animation; + late Sprite sprite; + + setUp(() { + controller = MockSpriteAnimationController(); + animation = MockSpriteAnimation(); + sprite = MockSprite(); + + when(() => controller.animation).thenAnswer((_) => animation); + + when(() => animation.totalDuration()).thenAnswer((_) => 1); + when(() => animation.getSprite()).thenAnswer((_) => sprite); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + when(() => sprite.srcSize).thenAnswer((_) => Vector2(1, 1)); + }); + + testWidgets('renders correctly', (tester) async { + await tester.pumpWidget( + SpriteAnimationWidget( + controller: controller, + ), + ); + + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect(find.byType(SpriteAnimationWidget), findsOneWidget); + }); + + test('SpriteAnimationController is updating animations', () { + SpriteAnimationController( + vsync: const TestVSync(), + animation: animation, + ).notifyListeners(); + + verify(() => animation.update(any())).called(1); + }); + + testWidgets('SpritePainter shouldRepaint returns true when Sprite changed', + (tester) async { + final spritePainter = SpritePainter( + sprite, + Anchor.center, + angle: 45, + ); + + final anotherPainter = SpritePainter( + sprite, + Anchor.center, + angle: 30, + ); + + expect(spritePainter.shouldRepaint(anotherPainter), isTrue); + }); + }); +} From 06b7f2983036b5b668e2cafb2e47e98e4bc042b1 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Mon, 25 Apr 2022 15:54:32 +0100 Subject: [PATCH 2/2] feat: removed `Component` inheritance form `Blueprint` (#218) --- .../test/src/components/spaceship_ramp_test.dart | 7 +++++-- packages/pinball_flame/lib/src/blueprint.dart | 15 ++++++--------- test/game/components/sparky_fire_zone_test.dart | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart index 98a6e95f..a65ba18b 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp_test.dart @@ -29,9 +29,12 @@ void main() { 'loads correctly', (game) async { final spaceshipRamp = SpaceshipRamp(); - await game.ensureAdd(spaceshipRamp); + await game.addFromBlueprint(spaceshipRamp); + await game.ready(); - expect(game.contains(spaceshipRamp), isTrue); + for (final component in spaceshipRamp.components) { + expect(game.contains(component), isTrue); + } }, ); diff --git a/packages/pinball_flame/lib/src/blueprint.dart b/packages/pinball_flame/lib/src/blueprint.dart index de9c8b2c..c7bd5a5e 100644 --- a/packages/pinball_flame/lib/src/blueprint.dart +++ b/packages/pinball_flame/lib/src/blueprint.dart @@ -1,16 +1,13 @@ import 'package:flame/components.dart'; import 'package:flame/game.dart'; -// TODO(erickzanardo): Keeping this inside our code base -// so we can experiment with the idea, but this is a -// potential upstream change on Flame. +// TODO(erickzanardo): Keeping this inside our code base so we can experiment +// with the idea, but this is a potential upstream change on Flame. /// {@template blueprint} -/// A [Blueprint] is a virtual way of grouping [Component]s that are related, -/// but they need to be added directly on the [FlameGame] level. +/// A [Blueprint] is a virtual way of grouping [Component]s that are related. /// {@endtemplate blueprint} -// TODO(alestiago): refactor with feat/make-blueprint-extend-component. -class Blueprint extends Component { +class Blueprint { /// {@macro blueprint} Blueprint({ Iterable? components, @@ -27,7 +24,7 @@ class Blueprint extends Component { final List _components = []; - final List _blueprints = []; + final List _blueprints = []; Future _addToParent(Component parent) async { await parent.addAll(_components); @@ -37,7 +34,7 @@ class Blueprint extends Component { List get components => List.unmodifiable(_components); /// Returns a copy of the blueprints built by this blueprint. - List get blueprints => List.unmodifiable(_blueprints); + List get blueprints => List.unmodifiable(_blueprints); } /// Adds helper methods regarding [Blueprint]s to [FlameGame]. diff --git a/test/game/components/sparky_fire_zone_test.dart b/test/game/components/sparky_fire_zone_test.dart index e2bc08b3..0ad69dab 100644 --- a/test/game/components/sparky_fire_zone_test.dart +++ b/test/game/components/sparky_fire_zone_test.dart @@ -27,8 +27,8 @@ void main() { group('SparkyFireZone', () { flameTester.test('loads correctly', (game) async { - final sparkyFireZone = SparkyFireZone(); - await game.ensureAdd(sparkyFireZone); + await game.addFromBlueprint(SparkyFireZone()); + await game.ready(); }); group('loads', () {