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 000000000..93afd3e02 Binary files /dev/null and b/doc/examples/animation_widget/assets/images/minotaur.png differ diff --git a/doc/examples/animation_widget/lib/main.dart b/doc/examples/animation_widget/lib/main.dart new file mode 100644 index 000000000..7ed529191 --- /dev/null +++ b/doc/examples/animation_widget/lib/main.dart @@ -0,0 +1,62 @@ +import 'package:flame/animation.dart' as animation; +import 'package:flame/flame.dart'; +import 'package:flame/position.dart'; +import 'package:flutter/material.dart'; + +void main() => 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}) =>