Adding SimpleGame, EmbeddedGameWidget and animationAsWidget to allow for easy integration of flame with non-game flutter apps, plus docs improved, minor fixes

This commit is contained in:
Luan Nico
2019-01-24 21:31:49 -02:00
parent 60bb83d388
commit 45455f60f0
19 changed files with 313 additions and 38 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
# animation_widget
A sample Flame project to showcase the animationAsWidget method to render easy sprite sheet animations on

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -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<MyHomePage> {
void _clickFab(GlobalKey<ScaffoldState> key) {
key.currentState.showSnackBar(new SnackBar(
content: new Text('You clicked the FAB!'),
));
}
@override
Widget build(BuildContext context) {
final key = new GlobalKey<ScaffoldState>();
return Scaffold(
key: key,
appBar: AppBar(
title: Text('Animation as a Widget Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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),
),
);
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -10,7 +10,7 @@ dependencies:
flutter:
sdk: flutter
flame:
path: ../../
path: ../../../
dev_dependencies:
flutter_test:

View File

@ -10,7 +10,7 @@ dependencies:
flutter:
sdk: flutter
flame:
path: ../..
path: ../../../
dev_dependencies:
flutter_test:

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -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();
}

View File

@ -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<StatefulWidget> createState() {
return new _EmbeddedGameWidgetState(game, size: size);
}
}
class _EmbeddedGameWidgetState extends State<EmbeddedGameWidget> {
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),
);
}
}

View File

@ -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));
}
}

View File

@ -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,

View File

@ -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<void> 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);
}
}

View File

@ -17,8 +17,7 @@ void main() {
class TestAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async =>
new File('assets/map-level1.png')
Future<ByteData> load(String key) async => new File('assets/map-level1.png')
.readAsBytes()
.then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer));