diff --git a/doc/examples/animation_widget/lib/main.dart b/doc/examples/animation_widget/lib/main.dart index 5daa91634..809bbb4ab 100644 --- a/doc/examples/animation_widget/lib/main.dart +++ b/doc/examples/animation_widget/lib/main.dart @@ -14,7 +14,8 @@ SpriteAnimation _animation; void main() async { WidgetsFlutterBinding.ensureInitialized(); - _sprite = await Sprite.loadSprite('minotaur.png', width: 96, height: 96); + final image = await Flame.images.load('minotaur.png'); + _sprite = Sprite(image, width: 96, height: 96); await Flame.images.load('minotaur.png'); final _animationSpriteSheet = SpriteSheet( diff --git a/doc/examples/animations/lib/main.dart b/doc/examples/animations/lib/main.dart index 8f86e2e54..dc1d8d83f 100644 --- a/doc/examples/animations/lib/main.dart +++ b/doc/examples/animations/lib/main.dart @@ -4,7 +4,8 @@ import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flame/sprite_animation.dart'; import 'package:flame/components/sprite_animation_component.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Image; +import 'dart:ui'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -16,14 +17,24 @@ void main() async { } class MyGame extends BaseGame with TapDetector { - final animation = SpriteAnimation.sequenced( - 'chopper.png', - 4, - textureWidth: 48, - textureHeight: 48, - stepTime: 0.15, - loop: true, - ); + Image chopper; + Image creature; + SpriteAnimation animation; + + @override + Future onLoad() async { + chopper = await Flame.images.load('chopper.png'); + creature = await Flame.images.load('creature.png'); + + animation = SpriteAnimation.sequenced( + chopper, + 4, + textureWidth: 48, + textureHeight: 48, + stepTime: 0.15, + loop: true, + ); + } void addAnimation(double x, double y) { const textureWidth = 291.0; @@ -32,7 +43,7 @@ class MyGame extends BaseGame with TapDetector { final animationComponent = SpriteAnimationComponent.sequenced( 291, 178, - 'creature.png', + creature, 18, amountPerRow: 10, textureWidth: textureWidth, diff --git a/doc/examples/aseprite/lib/main.dart b/doc/examples/aseprite/lib/main.dart index bf17d2bdf..30ede7967 100644 --- a/doc/examples/aseprite/lib/main.dart +++ b/doc/examples/aseprite/lib/main.dart @@ -13,14 +13,16 @@ void main() async { class MyGame extends BaseGame { MyGame(Size screenSize) { size = screenSize; - _start(); } - void _start() async { + @override + Future onLoad() async { + final image = await Flame.images.load('chopper.png'); final animation = await SpriteAnimation.fromAsepriteData( - 'chopper.png', + image, 'chopper.json', ); + final animationComponent = SpriteAnimationComponent(200, 200, animation); animationComponent.x = (size.width / 2) - 100; diff --git a/lib/components/sprite_animation_component.dart b/lib/components/sprite_animation_component.dart index f912d8332..7308c4fd1 100644 --- a/lib/components/sprite_animation_component.dart +++ b/lib/components/sprite_animation_component.dart @@ -25,7 +25,7 @@ class SpriteAnimationComponent extends PositionComponent { SpriteAnimationComponent.sequenced( double width, double height, - String imagePath, + Image image, int amount, { int amountPerRow, double textureX = 0.0, @@ -39,7 +39,7 @@ class SpriteAnimationComponent extends PositionComponent { this.width = width; this.height = height; animation = SpriteAnimation.sequenced( - imagePath, + image, amount, amountPerRow: amountPerRow, textureX: textureX, @@ -51,8 +51,34 @@ class SpriteAnimationComponent extends PositionComponent { ); } - @override - bool loaded() => animation.loaded(); + SpriteAnimationComponent.variableSequenced( + double width, + double height, + Image image, + int amount, + List stepTimes, { + int amountPerRow, + double textureX = 0.0, + double textureY = 0.0, + double textureWidth, + double textureHeight, + bool loop = true, + }) { + this.width = width; + this.height = height; + + animation = SpriteAnimation.variableSequenced( + image, + amount, + stepTimes, + amountPerRow: amountPerRow, + textureX: textureX, + textureY: textureY, + textureWidth: textureWidth, + textureHeight: textureHeight, + loop: loop, + ); + } @override bool destroy() => destroyOnFinish && animation.isLastFrame; diff --git a/lib/components/sprite_component.dart b/lib/components/sprite_component.dart index 9d6b80571..565331394 100644 --- a/lib/components/sprite_component.dart +++ b/lib/components/sprite_component.dart @@ -22,11 +22,11 @@ class SpriteComponent extends PositionComponent { SpriteComponent(); - SpriteComponent.square(double size, String imagePath) - : this.rectangle(size, size, imagePath); + SpriteComponent.square(double size, Image image) + : this.rectangle(size, size, image); - SpriteComponent.rectangle(double width, double height, String imagePath) - : this.fromSprite(width, height, Sprite(imagePath)); + SpriteComponent.rectangle(double width, double height, Image image) + : this.fromSprite(width, height, Sprite(image)); SpriteComponent.fromSprite(double width, double height, this.sprite) { this.width = width; @@ -44,9 +44,4 @@ class SpriteComponent extends PositionComponent { overridePaint: overridePaint, ); } - - @override - bool loaded() { - return sprite?.loaded() == true && x != null && y != null; - } } diff --git a/lib/game/base_game.dart b/lib/game/base_game.dart index 1a46e4fe7..299b2e112 100644 --- a/lib/game/base_game.dart +++ b/lib/game/base_game.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import 'package:flame/components/composed_component.dart'; import 'package:flame/fps_counter.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart' hide WidgetBuilder; diff --git a/lib/game/game.dart b/lib/game/game.dart index ddde8b28e..46791ef7e 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -91,6 +91,12 @@ abstract class Game { VoidCallback pauseEngineFn; VoidCallback resumeEngineFn; + + /// Use this method to load the assets need for the game instance to run + Future onLoad() async {} + + /// Returns the widget which will be show while the instance is loading + Widget loadingWidget() => Container(); } class OverlayWidget { diff --git a/lib/game/widget_builder.dart b/lib/game/widget_builder.dart index 3f98abb86..bacfe0d02 100644 --- a/lib/game/widget_builder.dart +++ b/lib/game/widget_builder.dart @@ -224,7 +224,14 @@ class WidgetBuilder { widget = _applyAdvancedGesturesDetectors(game, widget); } - return widget; + return FutureBuilder( + future: game.onLoad(), + builder: (_, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return widget; + } + return game.loadingWidget(); + }); } } diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 000000000..11655b668 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + // This makes the visual density adapt to the platform that you run + // the app on. For desktop platforms, the controls will be smaller and + // closer together (more dense) than on mobile platforms. + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Invoke "debug painting" (press "p" in the console, choose the + // "Toggle Debug Paint" action from the Flutter Inspector in Android + // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) + // to see the wireframe for each widget. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/lib/sprite.dart b/lib/sprite.dart index 54c88469f..182c09487 100644 --- a/lib/sprite.dart +++ b/lib/sprite.dart @@ -1,7 +1,5 @@ import 'dart:ui'; -import 'dart:async'; -import 'flame.dart'; import 'position.dart'; import 'palette.dart'; @@ -11,67 +9,24 @@ class Sprite { Rect src; Sprite( - String fileName, { - double x = 0.0, - double y = 0.0, - double width, - double height, - }) { - Flame.images.load(fileName).then((img) { - width ??= img.width.toDouble(); - height ??= img.height.toDouble(); - image = img; - src = Rect.fromLTWH(x, y, width, height); - }); - } - - Sprite.fromImage( this.image, { double x = 0.0, double y = 0.0, double width, double height, - }) { + }) : assert(image != null, "image can't be null") { width ??= image.width.toDouble(); height ??= image.height.toDouble(); src = Rect.fromLTWH(x, y, width, height); } - static Future loadSprite( - String fileName, { - double x = 0.0, - double y = 0.0, - double width, - double height, - }) async { - final Image image = await Flame.images.load(fileName); - return Sprite.fromImage( - image, - x: x, - y: y, - width: width, - height: height, - ); - } - - bool loaded() { - return image != null && src != null; - } - double get _imageWidth => image.width.toDouble(); double get _imageHeight => image.height.toDouble(); - Position get originalSize { - if (!loaded()) { - return null; - } - return Position(_imageWidth, _imageHeight); - } + Position get originalSize => Position(_imageWidth, _imageHeight); - Position get size { - return Position(src.width, src.height); - } + Position get size => Position(src.width, src.height); /// Renders this Sprite on the position [p], scaled by the [scale] factor provided. /// @@ -80,18 +35,12 @@ class Sprite { /// If not loaded, does nothing. void renderScaled(Canvas canvas, Position p, {double scale = 1.0, Paint overridePaint}) { - if (!loaded()) { - return; - } renderPosition(canvas, p, size: size.times(scale), overridePaint: overridePaint); } void renderPosition(Canvas canvas, Position p, {Position size, Paint overridePaint}) { - if (!loaded()) { - return; - } size ??= this.size; renderRect(canvas, Position.rectFrom(p, size), overridePaint: overridePaint); @@ -99,9 +48,6 @@ class Sprite { void render(Canvas canvas, {double width, double height, Paint overridePaint}) { - if (!loaded()) { - return; - } width ??= size.x; height ??= size.y; renderRect(canvas, Rect.fromLTWH(0.0, 0.0, width, height), @@ -114,9 +60,6 @@ class Sprite { /// If the asset is not yet loaded, it does nothing. void renderCentered(Canvas canvas, Position p, {Position size, Paint overridePaint}) { - if (!loaded()) { - return; - } size ??= this.size; renderRect(canvas, Rect.fromLTWH(p.x - size.x / 2, p.y - size.y / 2, size.x, size.y), @@ -124,9 +67,6 @@ class Sprite { } void renderRect(Canvas canvas, Rect dst, {Paint overridePaint}) { - if (!loaded()) { - return; - } canvas.drawImageRect(image, src, dst, overridePaint ?? paint); } } diff --git a/lib/sprite_animation.dart b/lib/sprite_animation.dart index 1fbc7ce34..58fa66018 100644 --- a/lib/sprite_animation.dart +++ b/lib/sprite_animation.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'flame.dart'; import 'sprite.dart'; @@ -72,7 +73,7 @@ class SpriteAnimation { /// Animation.sequenced('sheet.png', 8, textureY: 32.0 * i, textureWidth: 32.0, textureHeight: 32.0); /// This will create the i-th animation on the 'sheet.png', given it has 8 frames. SpriteAnimation.sequenced( - String imagePath, + Image image, int amount, { int amountPerRow, double textureX = 0.0, @@ -80,25 +81,22 @@ class SpriteAnimation { double textureWidth, double textureHeight, double stepTime = 0.1, - this.loop = true, - }) : assert(amountPerRow == null || amount >= amountPerRow) { - amountPerRow ??= amount; - frames = List(amount); - for (var i = 0; i < amount; i++) { - final Sprite sprite = Sprite( - imagePath, - x: textureX + (i % amountPerRow) * textureWidth, - y: textureY + (i ~/ amountPerRow) * textureHeight, - width: textureWidth, - height: textureHeight, - ); - frames[i] = SpriteAnimationFrame(sprite, stepTime); - } - } + bool loop = true, + }) : this.variableSequenced( + image, + amount, + List.generate(amount, (_) => stepTime), + amountPerRow: amountPerRow, + textureX: textureX, + textureY: textureX, + textureWidth: textureWidth, + textureHeight: textureHeight, + loop: loop, + ); /// Works just like [SpriteAnimation.sequenced], but it takes a list of variable [stepTimes], associating each one with one frame in the sequence. SpriteAnimation.variableSequenced( - String imagePath, + Image image, int amount, List stepTimes, { int amountPerRow, @@ -112,7 +110,7 @@ class SpriteAnimation { frames = List(amount); for (var i = 0; i < amount; i++) { final Sprite sprite = Sprite( - imagePath, + image, x: textureX + (i % amountPerRow) * textureWidth, y: textureY + (i ~/ amountPerRow) * textureHeight, width: textureWidth, @@ -128,7 +126,9 @@ class SpriteAnimation { /// [imagePath]: Source of the sprite sheet animation /// [dataPath]: Animation's exported data in json format static Future fromAsepriteData( - String imagePath, String dataPath) async { + Image image, + String dataPath, + ) async { final String content = await Flame.assets.readFile(dataPath); final Map json = jsonDecode(content); @@ -144,7 +144,7 @@ class SpriteAnimation { final stepTime = value['duration'] / 1000; final Sprite sprite = Sprite( - imagePath, + image, x: x.toDouble(), y: y.toDouble(), width: width.toDouble(), @@ -230,11 +230,6 @@ class SpriteAnimation { return SpriteAnimation(frames.reversed.toList(), loop: loop); } - /// Whether all sprites composing this animation are loaded. - bool loaded() { - return frames.every((frame) => frame.sprite.loaded()); - } - /// Computes the total duration of this animation (before it's done or repeats). double totalDuration() { return frames.map((f) => f.stepTime).reduce((a, b) => a + b);