From 45455f60f0d57356ee366cf3bfb017de8ae832c4 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Thu, 24 Jan 2019 21:31:49 -0200 Subject: [PATCH] Adding SimpleGame, EmbeddedGameWidget and animationAsWidget to allow for easy integration of flame with non-game flutter apps, plus docs improved, minor fixes --- doc/examples/animation_widget/.gitignore | 71 ++++++++++++++ doc/examples/animation_widget/.metadata | 10 ++ doc/examples/animation_widget/README.md | 3 + .../assets/images/minotaur.png | Bin 0 -> 4092 bytes doc/examples/animation_widget/lib/main.dart | 62 ++++++++++++ doc/examples/animation_widget/pubspec.yaml | 22 +++++ doc/examples/text/lib/main.dart | 6 +- doc/examples/text/pubspec.yaml | 4 +- doc/examples/tiled/pubspec.yaml | 4 +- lib/anchor.dart | 5 +- lib/components/component.dart | 10 +- lib/components/text_box_component.dart | 26 ++++-- lib/components/text_component.dart | 2 +- lib/game.dart | 88 ++++++++++++++++-- lib/palette.dart | 2 +- lib/sprite.dart | 3 +- lib/text_config.dart | 9 +- lib/util.dart | 17 +++- test/components/tiled_test.dart | 7 +- 19 files changed, 313 insertions(+), 38 deletions(-) create mode 100644 doc/examples/animation_widget/.gitignore create mode 100644 doc/examples/animation_widget/.metadata create mode 100644 doc/examples/animation_widget/README.md create mode 100644 doc/examples/animation_widget/assets/images/minotaur.png create mode 100644 doc/examples/animation_widget/lib/main.dart create mode 100644 doc/examples/animation_widget/pubspec.yaml diff --git a/doc/examples/animation_widget/.gitignore b/doc/examples/animation_widget/.gitignore new file mode 100644 index 000000000..47e0b4d62 --- /dev/null +++ b/doc/examples/animation_widget/.gitignore @@ -0,0 +1,71 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/doc/examples/animation_widget/.metadata b/doc/examples/animation_widget/.metadata new file mode 100644 index 000000000..fd2a86fdd --- /dev/null +++ b/doc/examples/animation_widget/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b + channel: beta + +project_type: app diff --git a/doc/examples/animation_widget/README.md b/doc/examples/animation_widget/README.md new file mode 100644 index 000000000..6d3ab9cb3 --- /dev/null +++ b/doc/examples/animation_widget/README.md @@ -0,0 +1,3 @@ +# animation_widget + +A sample Flame project to showcase the animationAsWidget method to render easy sprite sheet animations on \ No newline at end of file diff --git a/doc/examples/animation_widget/assets/images/minotaur.png b/doc/examples/animation_widget/assets/images/minotaur.png new file mode 100644 index 0000000000000000000000000000000000000000..93afd3e02814769abc189234be31a62f3c657802 GIT binary patch literal 4092 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 runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Animation as a Widget Demo', + theme: ThemeData(primarySwatch: Colors.blue), + home: MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + void _clickFab(GlobalKey key) { + key.currentState.showSnackBar(new SnackBar( + content: new Text('You clicked the FAB!'), + )); + } + + @override + Widget build(BuildContext context) { + final key = new GlobalKey(); + return Scaffold( + key: key, + appBar: AppBar( + title: Text('Animation as a Widget Demo'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Hi there! This is a regular Flutter app,'), + Text('with a complex widget tree and also'), + Text('some pretty sprite sheet animations :)'), + Flame.util.animationAsWidget( + Position(256.0, 256.0), + animation.Animation.sequenced('minotaur.png', 19, + textureWidth: 96.0)), + Text('Neat, hum?'), + Text('Sprites from Elthen\'s amazing work on itch.io:'), + Text('https://elthen.itch.io/2d-pixel-art-minotaur-sprites'), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _clickFab(key), + child: Icon(Icons.add), + ), + ); + } +} diff --git a/doc/examples/animation_widget/pubspec.yaml b/doc/examples/animation_widget/pubspec.yaml new file mode 100644 index 000000000..cc60be880 --- /dev/null +++ b/doc/examples/animation_widget/pubspec.yaml @@ -0,0 +1,22 @@ +name: animation_widget +description: A sample Flame project to showcase the animationAsWidget method. + +version: 0.1.0 + +environment: + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flame: + path: ../../../ + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/images/minotaur.png # thanks https://elthen.itch.io/2d-pixel-art-minotaur-sprites diff --git a/doc/examples/text/lib/main.dart b/doc/examples/text/lib/main.dart index a80fa8f2b..9393d3f40 100644 --- a/doc/examples/text/lib/main.dart +++ b/doc/examples/text/lib/main.dart @@ -15,7 +15,8 @@ TextConfig regular = TextConfig(color: BasicPalette.white.color); TextConfig tiny = regular.withFontSize(12.0); class MyTextBox extends TextBoxComponent { - MyTextBox(String text) : super(text, config: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05)); + MyTextBox(String text) + : super(text, config: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05)); @override void drawBackground(Canvas c) { @@ -52,7 +53,8 @@ class MyGame extends BaseGame { ..x = size.width ..y = size.height); - add(MyTextBox('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget ligula eu lectus lobortis condimentum.') + add(MyTextBox( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget ligula eu lectus lobortis condimentum.') ..anchor = Anchor.bottomLeft ..y = size.height); } diff --git a/doc/examples/text/pubspec.yaml b/doc/examples/text/pubspec.yaml index cfb3b852a..de4ff0969 100644 --- a/doc/examples/text/pubspec.yaml +++ b/doc/examples/text/pubspec.yaml @@ -10,8 +10,8 @@ dependencies: flutter: sdk: flutter flame: - path: ../../ + path: ../../../ dev_dependencies: flutter_test: - sdk: flutter \ No newline at end of file + sdk: flutter diff --git a/doc/examples/tiled/pubspec.yaml b/doc/examples/tiled/pubspec.yaml index de0c71c97..b0026fe33 100644 --- a/doc/examples/tiled/pubspec.yaml +++ b/doc/examples/tiled/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter flame: - path: ../.. + path: ../../../ dev_dependencies: flutter_test: @@ -20,4 +20,4 @@ flutter: assets: - assets/tiles/map.tmx - assets/images/map-level1.png - - assets/images/map-level2.png \ No newline at end of file + - assets/images/map-level2.png diff --git a/lib/anchor.dart b/lib/anchor.dart index edfd9e7da..81c2b2f36 100644 --- a/lib/anchor.dart +++ b/lib/anchor.dart @@ -18,6 +18,7 @@ class Anchor { const Anchor(this.relativePosition); Position translate(Position p, Position size) { - return p.clone().minus(new Position(size.x * relativePosition.dx, size.y * relativePosition.dy)); + return p.clone().minus(new Position( + size.x * relativePosition.dx, size.y * relativePosition.dy)); } -} \ No newline at end of file +} diff --git a/lib/components/component.dart b/lib/components/component.dart index 5c8536f39..ff0d14a79 100644 --- a/lib/components/component.dart +++ b/lib/components/component.dart @@ -95,8 +95,8 @@ abstract class PositionComponent extends Component { canvas.translate(x, y); canvas.rotate(angle); - double dx = - anchor.relativePosition.dx * width; - double dy = - anchor.relativePosition.dy * height; + double dx = -anchor.relativePosition.dx * width; + double dy = -anchor.relativePosition.dy * height; canvas.translate(dx, dy); } } @@ -109,9 +109,11 @@ class SpriteComponent extends PositionComponent { SpriteComponent(); - SpriteComponent.square(double size, String imagePath) : this.rectangle(size, size, imagePath); + SpriteComponent.square(double size, String imagePath) + : this.rectangle(size, size, imagePath); - SpriteComponent.rectangle(double width, double height, String imagePath) : this.fromSprite(width, height, new Sprite(imagePath)); + SpriteComponent.rectangle(double width, double height, String imagePath) + : this.fromSprite(width, height, new Sprite(imagePath)); SpriteComponent.fromSprite(double width, double height, this.sprite) { this.width = width; diff --git a/lib/components/text_box_component.dart b/lib/components/text_box_component.dart index fda5140d6..ac4359ece 100644 --- a/lib/components/text_box_component.dart +++ b/lib/components/text_box_component.dart @@ -43,7 +43,9 @@ class TextBoxComponent extends PositionComponent with Resizable { TextBoxConfig get boxConfig => _boxConfig; - TextBoxComponent(String text, {TextConfig config = const TextConfig(), TextBoxConfig boxConfig = const TextBoxConfig()}) { + TextBoxComponent(String text, + {TextConfig config = const TextConfig(), + TextBoxConfig boxConfig = const TextBoxConfig()}) { _boxConfig = boxConfig; _config = config; _text = text; @@ -76,7 +78,9 @@ class TextBoxComponent extends PositionComponent with Resizable { bool get finished => _lifeTime > totalCharTime + _boxConfig.dismissDelay; - int get currentChar => _boxConfig.timePerChar == 0.0 ? _text.length - 1 : math.min(_lifeTime ~/ _boxConfig.timePerChar, _text.length - 1); + int get currentChar => _boxConfig.timePerChar == 0.0 + ? _text.length - 1 + : math.min(_lifeTime ~/ _boxConfig.timePerChar, _text.length - 1); int get currentLine { int totalCharCount = 0; @@ -103,7 +107,9 @@ class TextBoxComponent extends PositionComponent with Resizable { double get totalHeight => _withMargins(_lineHeight * _lines.length); double getLineWidth(String line, int charCount) { - return _withMargins(_config.toTextPainter(line.substring(0, math.min(charCount, line.length))).width); + return _withMargins(_config + .toTextPainter(line.substring(0, math.min(charCount, line.length))) + .width); } double get currentWidth { @@ -112,7 +118,8 @@ class TextBoxComponent extends PositionComponent with Resizable { int _currentChar = currentChar; int _currentLine = currentLine; return _lines.sublist(0, _currentLine + 1).map((line) { - int charCount = (i < _currentLine) ? line.length : (_currentChar - totalCharCount); + int charCount = + (i < _currentLine) ? line.length : (_currentChar - totalCharCount); totalCharCount += line.length; i++; return getLineWidth(line, charCount); @@ -128,7 +135,8 @@ class TextBoxComponent extends PositionComponent with Resizable { Image _redrawCache() { PictureRecorder recorder = new PictureRecorder(); - Canvas c = new Canvas(recorder, new Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble())); + Canvas c = new Canvas(recorder, + new Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble())); _fullRender(c); return recorder.endRecording().toImage(width.toInt(), height.toInt()); } @@ -143,11 +151,15 @@ class TextBoxComponent extends PositionComponent with Resizable { double dy = _boxConfig.margin; for (int line = 0; line < _currentLine; line++) { charCount += _lines[line].length; - _config.toTextPainter(_lines[line]).paint(c, new Offset(_boxConfig.margin, dy)); + _config + .toTextPainter(_lines[line]) + .paint(c, new Offset(_boxConfig.margin, dy)); dy += _lineHeight; } int max = math.min(currentChar - charCount, _lines[_currentLine].length); - _config.toTextPainter(_lines[_currentLine].substring(0, max)).paint(c, new Offset(_boxConfig.margin, dy)); + _config + .toTextPainter(_lines[_currentLine].substring(0, max)) + .paint(c, new Offset(_boxConfig.margin, dy)); } void update(double dt) { diff --git a/lib/components/text_component.dart b/lib/components/text_component.dart index 780690088..264c3d583 100644 --- a/lib/components/text_component.dart +++ b/lib/components/text_component.dart @@ -24,7 +24,7 @@ class TextComponent extends PositionComponent { _updateBox(); } - TextComponent(this._text, { TextConfig config = const TextConfig() }) { + TextComponent(this._text, {TextConfig config = const TextConfig()}) { this._config = config; _updateBox(); } diff --git a/lib/game.dart b/lib/game.dart index f9d1854b4..dd6e01580 100644 --- a/lib/game.dart +++ b/lib/game.dart @@ -38,6 +38,7 @@ abstract class Game { void _recordDt(double dt) {} + Offset _offset = Offset.zero; Widget _widget; /// Returns the game widget. Put this in your structure to start rendering and updating the game. @@ -135,7 +136,10 @@ class _GameRenderBox extends RenderBox with WidgetsBindingObserver { @override void paint(PaintingContext context, Offset offset) { + context.canvas.save(); + context.canvas.translate(game._offset.dx, game._offset.dy); game.render(context.canvas); + context.canvas.restore(); } void _bindLifecycleListener() { @@ -177,7 +181,8 @@ abstract class BaseGame extends Game { /// This method is called for every component added, both via [add] and [addLater] methods. /// /// You can use this to setup your mixins, pre-calculate stuff on every component, or anything you desire. - /// By default this calls the first time resize for every component, so don't forget to call super.preAdd when overriding. + /// By default, this calls the first time resize for every component, so don't forget to call super.preAdd when overriding. + @mustCallSuper void preAdd(Component c) { // first time resize if (size != null) { @@ -187,7 +192,7 @@ abstract class BaseGame extends Game { /// Adds a new component to the components list. /// - /// Also calls [preAdd], witch in turn sets the current size on the component (because the resize hook won't be called). + /// Also calls [preAdd], witch in turn sets the current size on the component (because the resize hook won't be called until a new resize happens). void add(Component c) { this.preAdd(c); this.components.add(c); @@ -196,7 +201,7 @@ abstract class BaseGame extends Game { /// Registers a component to be added on the components on the next tick. /// /// Use this to add components in places where a concurrent issue with the update method might happen. - /// Also calls [preAdd] for the component added. + /// Also calls [preAdd] for the component added, immediately. void addLater(Component c) { this.preAdd(c); this._addLater.add(c); @@ -205,6 +210,7 @@ abstract class BaseGame extends Game { /// This implementation of render basically calls [renderComponent] for every component, making sure the canvas is reset for each one. /// /// You can override it further to add more custom behaviour. + /// Beware of however you are rendering components if not using this; you must be careful to save and restore the canvas to avoid components messing up with each other. @override void render(Canvas canvas) { canvas.save(); @@ -231,7 +237,7 @@ abstract class BaseGame extends Game { /// This implementation of update updates every component in the list. /// /// It also actually adds the components that were added by the [addLater] method, and remove those that are marked for destruction via the [Component.destroy] method. - /// You can override it futher to add more custom behaviour. + /// You can override it further to add more custom behaviour. @override void update(double t) { components.addAll(_addLater); @@ -241,10 +247,12 @@ abstract class BaseGame extends Game { components.removeWhere((c) => c.destroy()); } - /// This implementation of resize repasses the resize call to every component in the list, enabling each one to make their decisions as how to handle the resize. + /// This implementation of resize passes the resize call along to every component in the list, enabling each one to make their decisions as how to handle the resize. /// - /// You can override it futher to add more custom behaviour. + /// It also updates the [size] field of the class to be used by later added components and other methods. + /// You can override it further to add more custom behaviour, but you should seriously consider calling the super implementation as well. @override + @mustCallSuper void resize(Size size) { this.size = size; components.forEach((c) => c.resize(size)); @@ -255,7 +263,7 @@ abstract class BaseGame extends Game { /// Returns `false` by default. Override to use the debug mode. /// In debug mode, the [_recordDt] method actually records every `dt` for statistics. /// Then, you can use the [fps] method to check the game FPS. - /// You can also use this value to enable other debug behaviors for your game. + /// You can also use this value to enable other debug behaviors for your game, like bounding box rendering, for instance. bool debugMode() => false; /// This is a hook that comes from the RenderBox to allow recording of render times and statistics. @@ -290,3 +298,69 @@ abstract class BaseGame extends Game { Duration.microsecondsPerSecond; } } + +/// This is a helper implementation of a [BaseGame] designed to allow to easily create a game with a single component. +/// +/// This is useful to add sprites, animations and other Flame components "directly" to your non-game Flutter widget tree, when combined with [EmbeddedGameWidget]. +class SimpleGame extends BaseGame { + SimpleGame(Component c) { + add(c); + } +} + +/// This a widget to embed a game inside the Widget tree. You can use it in pair with [SimpleGame] or any other more complex [Game], as desired. +/// +/// It handles for you positioning, size constraints and other factors that arise when your game is embedded within the component tree. +/// Provided it with a [Game] instance for your game and the optional size of the widget. +/// Creating this without a fixed size might mess up how other components are rendered with relation to this one in the tree. +/// You can bind Gesture Recognizers immediately around this to add controls to your widgets, with easy coordinate conversions. +class EmbeddedGameWidget extends StatefulWidget { + final Game game; + final Position size; + + EmbeddedGameWidget(this.game, {this.size}); + + @override + State createState() { + return new _EmbeddedGameWidgetState(game, size: size); + } +} + +class _EmbeddedGameWidgetState extends State { + final Game game; + final Position size; + + _EmbeddedGameWidgetState(this.game, {this.size}); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); + } + + @override + void didUpdateWidget(EmbeddedGameWidget oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); + } + + void _afterLayout(_) { + RenderBox box = context.findRenderObject(); + game._offset = box.localToGlobal(Offset.zero); + } + + @override + Widget build(BuildContext context) { + if (size == null) { + return game.widget; + } + return Container( + child: game.widget, + constraints: BoxConstraints( + minWidth: size.x, + maxWidth: size.x, + minHeight: size.y, + maxHeight: size.y), + ); + } +} diff --git a/lib/palette.dart b/lib/palette.dart index e6fb0faf1..b88b46126 100644 --- a/lib/palette.dart +++ b/lib/palette.dart @@ -27,4 +27,4 @@ class PaletteEntry { class BasicPalette { static const PaletteEntry white = PaletteEntry(Color(0xFFFFFFFF)); static const PaletteEntry black = PaletteEntry(Color(0xFF000000)); -} \ No newline at end of file +} diff --git a/lib/sprite.dart b/lib/sprite.dart index d8f3ff54e..fba12d69b 100644 --- a/lib/sprite.dart +++ b/lib/sprite.dart @@ -126,6 +126,7 @@ class Sprite { return; } size ??= this.size; - renderRect(canvas, new Rect.fromLTWH(p.x - size.x / 2, p.y - size.y / 2, size.x, size.y)); + renderRect(canvas, + new Rect.fromLTWH(p.x - size.x / 2, p.y - size.y / 2, size.x, size.y)); } } diff --git a/lib/text_config.dart b/lib/text_config.dart index 8e460c260..a3e1e2ed0 100644 --- a/lib/text_config.dart +++ b/lib/text_config.dart @@ -9,7 +9,6 @@ import 'anchor.dart'; /// It does not hold information regarding the position of the text to be render neither the text itself (the string). /// To hold all those information, use the Text component. class TextConfig { - /// The font size to be used, in points. final double fontSize; @@ -70,9 +69,11 @@ class TextConfig { /// /// const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font', anchor: Anchor.rightBottom); /// config.render(c, Offset(size.width - 10, size.height - 10); - void render(Canvas canvas, String text, Position p, { Anchor anchor: Anchor.topLeft }) { + void render(Canvas canvas, String text, Position p, + {Anchor anchor: Anchor.topLeft}) { material.TextPainter tp = toTextPainter(text); - Position translatedPosition = anchor.translate(p, Position.fromSize(tp.size)); + Position translatedPosition = + anchor.translate(p, Position.fromSize(tp.size)); tp.paint(canvas, translatedPosition.toOffset()); } @@ -149,7 +150,7 @@ class TextConfig { /// Creates a new [TextConfig] changing only the [textAlign]. /// /// This does not change the original (as it's immutable). - TextConfig withTextAlign (TextAlign textAlign) { + TextConfig withTextAlign(TextAlign textAlign) { return TextConfig( fontSize: fontSize, color: color, diff --git a/lib/util.dart b/lib/util.dart index 8e75b38ab..2daab8218 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -3,7 +3,11 @@ import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; +import 'animation.dart'; +import 'components/animation_component.dart'; +import 'game.dart'; import 'position.dart'; /// Some utilities that did not fit anywhere else. @@ -17,7 +21,7 @@ class Util { return SystemChrome.setEnabledSystemUIOverlays([]); } - /// Sets the preferred orietation (landscape or protrait for the app). + /// Sets the preferred orientation (landscape or portrait for the app). /// /// When it opens, it will automatically change orientation to the preferred one (if possible). Future setOrientation(DeviceOrientation orientation) { @@ -68,4 +72,15 @@ class Util { fn(c); c.translate(-p.x, -p.y); } + + /// Returns a regular Flutter widget representing this animation, rendered with the specified size. + /// + /// This actually creates an [EmbeddedGameWidget] with a [SimpleGame] whose only content is an [AnimationComponent] created from the provided [animation]. + /// You can use this implementation as base to easily create your own widgets based on more complex games. + /// This is intended to be used by non-game apps that want to add a sprite sheet animation. + widgets.Widget animationAsWidget(Position size, Animation animation) { + return EmbeddedGameWidget( + SimpleGame(AnimationComponent(size.x, size.y, animation)), + size: size); + } } diff --git a/test/components/tiled_test.dart b/test/components/tiled_test.dart index 763b61676..dbc306ba1 100644 --- a/test/components/tiled_test.dart +++ b/test/components/tiled_test.dart @@ -17,10 +17,9 @@ void main() { class TestAssetBundle extends CachingAssetBundle { @override - Future load(String key) async => - new File('assets/map-level1.png') - .readAsBytes() - .then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer)); + Future load(String key) async => new File('assets/map-level1.png') + .readAsBytes() + .then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer)); @override Future loadString(String key, {bool cache = true}) =>