Merge pull request #440 from flame-engine/luan.composed-p3

[v1] Part 3: Great Composed Component Refactor
This commit is contained in:
Luan Nico
2020-08-18 23:43:54 -04:00
committed by GitHub
32 changed files with 391 additions and 304 deletions

View File

@ -17,8 +17,7 @@ class Square extends PositionComponent {
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
final rect = Rect.fromLTWH(0, 0, width, height);
canvas.drawRect(rect, _paint);
super.render(canvas);
canvas.drawRect(toOriginRect(), _paint);
}
}

View File

@ -17,8 +17,7 @@ class Square extends PositionComponent {
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
final rect = Rect.fromLTWH(0, 0, width, height);
canvas.drawRect(rect, _paint);
super.render(canvas);
canvas.drawRect(toOriginRect(), _paint);
}
}

View File

@ -17,8 +17,7 @@ class Square extends PositionComponent {
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
final rect = Rect.fromLTWH(0, 0, width, height);
canvas.drawRect(rect, _paint);
super.render(canvas);
canvas.drawRect(toOriginRect(), _paint);
}
}

View File

@ -12,6 +12,7 @@ class Square extends PositionComponent {
@override
void render(Canvas canvas) {
canvas.drawRect(toRect(), _paint);
super.render(canvas);
canvas.drawRect(toOriginRect(), _paint);
}
}

View File

@ -29,7 +29,8 @@ class TapableSquare extends PositionComponent with Tapable {
@override
void render(Canvas canvas) {
canvas.drawRect(toRect(), _beenPressed ? _grey : _white);
super.render(canvas);
canvas.drawRect(toOriginRect(), _beenPressed ? _grey : _white);
}
@override

View File

@ -76,16 +76,13 @@ class Player extends Component implements JoystickListener {
void moveFromAngle(double dtUpdate) {
final double nextX = (currentSpeed * dtUpdate) * cos(radAngle);
final double nextY = (currentSpeed * dtUpdate) * sin(radAngle);
final Offset nextPoint = Offset(nextX, nextY);
final Offset diffBase = Offset(
_rect.center.dx + nextPoint.dx,
_rect.center.dy + nextPoint.dy,
_rect.center.dx + nextX,
_rect.center.dy + nextY,
) -
_rect.center;
final Rect newPosition = _rect.shift(diffBase);
_rect = newPosition;
_rect = _rect.shift(diffBase);
}
}

View File

@ -19,7 +19,8 @@ class Ball extends PositionComponent {
@override
void render(Canvas c) {
c.drawOval(toRect(), paint);
super.render(c);
c.drawOval(toOriginRect(), paint);
}
@override
@ -43,17 +44,16 @@ class Ball extends PositionComponent {
class MyGame extends BaseGame {
MyGame(Size screenSize) {
size = screenSize;
_start();
}
void _start() async {
Flame.audio.disableLog();
Flame.audio.load('boin.mp3');
Flame.audio.loop('music.mp3', volume: 0.4);
add(Ball(size)
..y = (size.height / 2) - 50
..width = 100
..height = 100);
add(
Ball(size)
..y = (size.height / 2) - 50
..width = 100
..height = 100,
);
}
}

View File

@ -52,7 +52,8 @@ class MyGame extends BaseGame {
..y = size.height);
add(MyTextBox(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget ligula eu lectus lobortis condimentum.')
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget ligula eu lectus lobortis condimentum.',
)
..anchor = Anchor.bottomLeft
..y = size.height);
}

View File

@ -16,14 +16,14 @@ class GameWidget extends StatelessWidget {
return MaterialApp(routes: {
'/': (BuildContext context) => Column(children: [
RaisedButton(
child: const Text("Game"),
child: const Text('Game'),
onPressed: () {
Navigator.of(context).pushNamed("/game");
Navigator.of(context).pushNamed('/game');
}),
RaisedButton(
child: const Text("BaseGame"),
child: const Text('BaseGame'),
onPressed: () {
Navigator.of(context).pushNamed("/base_game");
Navigator.of(context).pushNamed('/base_game');
})
]),
'/game': (BuildContext context) => MyGame().widget,
@ -40,7 +40,10 @@ class RenderedTimeComponent extends TimerComponent {
@override
void render(Canvas canvas) {
textConfig.render(
canvas, "Elapsed time: ${timer.current}", Position(10, 150));
canvas,
'Elapsed time: ${timer.current}',
Position(10, 150),
);
}
}
@ -84,8 +87,11 @@ class MyGame extends Game with TapDetector {
@override
void render(Canvas canvas) {
textConfig.render(canvas, "Countdown: ${countdown.current.toString()}",
Position(10, 100));
textConfig.render(canvas, "Elapsed time: $elapsedSecs", Position(10, 150));
textConfig.render(
canvas,
'Countdown: ${countdown.current}',
Position(10, 100),
);
textConfig.render(canvas, 'Elapsed time: $elapsedSecs', Position(10, 150));
}
}

View File

@ -109,7 +109,8 @@ void main() async {
child: SpriteWidget(
sprite: shieldSprite,
anchor: parseAnchor(
ctx.listProperty('anchor', 'Anchor.center', anchorOptions)),
ctx.listProperty('anchor', 'Anchor.center', anchorOptions),
),
),
),
);
@ -122,8 +123,12 @@ void main() async {
columns: 4,
rows: 1,
);
final _animation = _animationSpriteSheet.createAnimation(0,
stepTime: 0.2, to: 3, loop: true);
final _animation = _animationSpriteSheet.createAnimation(
0,
stepTime: 0.2,
to: 3,
loop: true,
);
dashbook.storiesOf('AnimationWidget').decorator(CenterDecorator()).add(
'default',
(ctx) => Container(
@ -133,7 +138,8 @@ void main() async {
animation: _animation,
playing: ctx.boolProperty('playing', true),
anchor: parseAnchor(
ctx.listProperty('anchor', 'Anchor.center', anchorOptions)),
ctx.listProperty('anchor', 'Anchor.center', anchorOptions),
),
),
),
);

View File

@ -23,15 +23,16 @@ class ExampleGame extends Game with HasWidgetsOverlay, TapDetector {
isPaused = false;
} else {
addWidgetOverlay(
'PauseMenu',
Center(
child: Container(
width: 100,
height: 100,
color: const Color(0xFFFF0000),
child: const Center(child: const Text('Paused')),
),
));
'PauseMenu',
Center(
child: Container(
width: 100,
height: 100,
color: const Color(0xFFFF0000),
child: const Center(child: const Text('Paused')),
),
),
);
isPaused = true;
}
}

View File

@ -27,14 +27,15 @@ class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Testing addingOverlay'),
),
body: _myGame == null ? const Text('Wait') : _myGame.widget,
floatingActionButton: FloatingActionButton(
onPressed: () => newGame(),
child: const Icon(Icons.add),
));
appBar: AppBar(
title: const Text('Testing addingOverlay'),
),
body: _myGame == null ? const Text('Wait') : _myGame.widget,
floatingActionButton: FloatingActionButton(
onPressed: () => newGame(),
child: const Icon(Icons.add),
),
);
}
void newGame() {

View File

@ -23,14 +23,17 @@ class Palette {
class Square extends PositionComponent with HasGameRef<MyGame> {
static const SPEED = 0.25;
static Paint white = Palette.white.paint;
static Paint red = Palette.red.paint;
static Paint blue = Palette.blue.paint;
@override
void render(Canvas c) {
prepareCanvas(c);
super.render(c);
c.drawRect(Rect.fromLTWH(0, 0, width, height), Palette.white.paint);
c.drawRect(const Rect.fromLTWH(0, 0, 3, 3), Palette.red.paint);
c.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), Palette.blue.paint);
c.drawRect(toOriginRect(), white);
c.drawRect(const Rect.fromLTWH(0, 0, 3, 3), red);
c.drawRect(Rect.fromLTWH(width / 2, height / 2, 3, 3), blue);
}
@override

View File

@ -19,6 +19,7 @@ class Anchor {
Position translate(Position p, Position size) {
return p.clone().minus(
Position(size.x * relativePosition.dx, size.y * relativePosition.dy));
Position(size.x * relativePosition.dx, size.y * relativePosition.dy),
);
}
}

View File

@ -1,100 +0,0 @@
import 'dart:ui';
import 'package:flame/game/base_game.dart';
import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/ordered_set.dart';
import 'component.dart';
import 'mixins/has_game_ref.dart';
import 'mixins/resizable.dart';
import 'mixins/tapable.dart';
/// A mixin that helps you to make a `Component` wraps other components. It is useful to group visual components through a hierarchy.
/// When implemented, makes every item in its `components` collection field be updated and rendered with the same conditions.
///
/// Example of usage, where visibility of two components are handled by a wrapper:
///
/// ```dart
/// class GameOverPanel extends PositionComponent with Resizable, ComposedComponent {
/// bool visible = false;
///
/// GameOverText gameOverText;
/// GameOverButton gameOverButton;
///
/// GameOverPanel(Image spriteImage) : super() {
/// gameOverText = GameOverText(spriteImage); // GameOverText is a Component
/// gameOverButton = GameOverButton(spriteImage); // GameOverRestart is a SpriteComponent
///
/// components..add(gameOverText)..add(gameOverButton);
/// }
///
/// @override
/// void render(Canvas canvas) {
/// if (visible) {
/// super.render(canvas);
/// } // If not, neither of its `components` will be rendered
/// }
/// }
/// ```
///
mixin ComposedComponent on Component, HasGameRef, Tapable {
OrderedSet<Component> components =
OrderedSet(Comparing.on((c) => c.priority()));
final List<Component> _removeLater = [];
@override
void render(Canvas canvas) {
canvas.save();
components.forEach((comp) => _renderComponent(canvas, comp));
canvas.restore();
}
void _renderComponent(Canvas canvas, Component c) {
if (!c.loaded()) {
return;
}
c.render(canvas);
canvas.restore();
canvas.save();
}
@override
void update(double t) {
_removeLater.forEach((c) => components.remove(c));
_removeLater.clear();
components.forEach((c) => c.update(t));
components.removeWhere((c) => c.destroy());
}
void add(Component c) {
if (gameRef is BaseGame) {
(gameRef as BaseGame).preAdd(c);
}
components.add(c);
}
void markToRemove(Component component) {
_removeLater.add(component);
}
// this is an important override for the Tapable mixin
@override
Iterable<Tapable> tapableChildren() => _findT<Tapable>();
// this is an important override for the Resizable mixin
Iterable<Resizable> resizableChildren() => _findT<Resizable>();
// Finds all children of type T, recursively
Iterable<T> _findT<T>() => components.expand((c) {
final List<T> r = [];
if (c is T) {
r.add(c as T);
}
if (c is ComposedComponent) {
r.addAll(c._findT<T>());
}
return r;
}).cast();
}

View File

@ -1,5 +1,7 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import '../flare_animation.dart';
import 'position_component.dart';
@ -8,7 +10,11 @@ class FlareComponent extends PositionComponent {
FlareAnimation _flareAnimation;
FlareComponent(
String fileName, String animation, double width, double height) {
String fileName,
String animation,
double width,
double height,
) {
this.width = width;
this.height = height;
@ -30,9 +36,10 @@ class FlareComponent extends PositionComponent {
@override
bool loaded() => _flareAnimation != null;
@mustCallSuper
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
super.render(canvas);
_flareAnimation.render(canvas, x: 0, y: 0);
}

View File

@ -1,12 +1,12 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import '../../game/base_game.dart';
import '../position_component.dart';
mixin Tapable {
Rect toRect();
mixin Tapable on PositionComponent {
void onTapCancel() {}
void onTapDown(TapDownDetails details) {}
void onTapUp(TapUpDetails details) {}
@ -15,22 +15,21 @@ mixin Tapable {
bool _checkPointerId(int pointerId) => _currentPointerId == pointerId;
bool checkTapOverlap(Offset o) => toRect().contains(o);
bool checkTapOverlap(Rect rect, Offset o) => rect.contains(o);
void handleTapDown(int pointerId, TapDownDetails details) {
if (checkTapOverlap(details.localPosition)) {
void handleTapDown(Rect rect, int pointerId, TapDownDetails details) {
if (checkTapOverlap(rect, details.localPosition)) {
_currentPointerId = pointerId;
onTapDown(details);
}
tapableChildren().forEach((c) => c.handleTapDown(pointerId, details));
}
void handleTapUp(int pointerId, TapUpDetails details) {
if (_checkPointerId(pointerId) && checkTapOverlap(details.localPosition)) {
void handleTapUp(Rect rect, int pointerId, TapUpDetails details) {
if (_checkPointerId(pointerId) &&
checkTapOverlap(rect, details.localPosition)) {
_currentPointerId = null;
onTapUp(details);
}
tapableChildren().forEach((c) => c.handleTapUp(pointerId, details));
}
void handleTapCancel(int pointerId) {
@ -38,28 +37,38 @@ mixin Tapable {
_currentPointerId = null;
onTapCancel();
}
tapableChildren().forEach((c) => c.handleTapCancel(pointerId));
}
/// Overwrite this to add children to this [Tapable].
///
/// If a [Tapable] has children, its children be taped as well.
Iterable<Tapable> tapableChildren() => [];
}
mixin HasTapableComponents on BaseGame {
Iterable<Tapable> get _tapableComponents =>
components.where((c) => c is Tapable).cast();
Iterable<PositionComponent> _positionComponents() {
return components.whereType<PositionComponent>();
}
@mustCallSuper
void onTapCancel(int pointerId) {
_tapableComponents.forEach((c) => c.handleTapCancel(pointerId));
_positionComponents().forEach((c) {
c.propagateToChildren<Tapable>(
(child, _) => child.handleTapCancel(pointerId),
);
});
}
@mustCallSuper
void onTapDown(int pointerId, TapDownDetails details) {
_tapableComponents.forEach((c) => c.handleTapDown(pointerId, details));
_positionComponents().forEach((c) {
c.propagateToChildren<Tapable>(
(child, rect) => child.handleTapDown(rect, pointerId, details),
);
});
}
@mustCallSuper
void onTapUp(int pointerId, TapUpDetails details) {
_tapableComponents.forEach((c) => c.handleTapUp(pointerId, details));
_positionComponents().forEach((c) {
c.propagateToChildren<Tapable>(
(child, rect) => child.handleTapUp(rect, pointerId, details),
);
});
}
}

View File

@ -1,5 +1,7 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import '../nine_tile_box.dart';
import 'position_component.dart';
@ -12,8 +14,10 @@ class NineTileBoxComponent extends PositionComponent {
/// It uses the x, y, width and height coordinates from the [PositionComponent] to render.
NineTileBoxComponent(this.nineTileBox);
@mustCallSuper
@override
void render(Canvas c) {
nineTileBox.drawRect(c, toRect());
super.render(c);
nineTileBox.drawRect(c, toOriginRect());
}
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import '../flame.dart';
@ -21,10 +22,12 @@ class ParallaxImage {
/// How to fill the screen with the image, always proportionally scaled.
final LayerFill fill;
ParallaxImage(this.filename,
{this.repeat = ImageRepeat.repeatX,
this.alignment = Alignment.bottomLeft,
this.fill = LayerFill.height});
ParallaxImage(
this.filename, {
this.repeat = ImageRepeat.repeatX,
this.alignment = Alignment.bottomLeft,
this.fill = LayerFill.height,
});
}
/// Represents one layer in the parallax, draws out an image on a canvas in the
@ -155,8 +158,11 @@ class ParallaxComponent extends PositionComponent {
List<ParallaxLayer> _layers;
bool _loaded = false;
ParallaxComponent(List<ParallaxImage> images,
{this.baseSpeed = Offset.zero, this.layerDelta = Offset.zero}) {
ParallaxComponent(
List<ParallaxImage> images, {
this.baseSpeed = Offset.zero,
this.layerDelta = Offset.zero,
}) {
_load(images);
}
@ -167,8 +173,10 @@ class ParallaxComponent extends PositionComponent {
@override
bool loaded() => _loaded;
@mustCallSuper
@override
void resize(Size size) {
super.resize(size);
_layers.forEach((layer) => layer.resize(size));
}
@ -178,18 +186,21 @@ class ParallaxComponent extends PositionComponent {
if (!loaded()) {
return;
}
_layers.forEach((layer) => layer
.update(baseSpeed * t + layerDelta * (_layers.indexOf(layer) * t)));
_layers.forEach((layer) {
layer.update(baseSpeed * t + layerDelta * (_layers.indexOf(layer) * t));
});
}
@mustCallSuper
@override
void render(Canvas canvas) {
if (!loaded()) {
return;
}
super.render(canvas);
canvas.save();
prepareCanvas(canvas);
_layers.forEach((layer) => layer.render(canvas));
canvas.restore();
}

View File

@ -2,9 +2,12 @@ import 'dart:ui';
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/ordered_set.dart';
import '../anchor.dart';
import '../effects/effects.dart';
import '../game.dart';
import '../position.dart';
import '../text_config.dart';
import 'component.dart';
@ -16,14 +19,50 @@ import 'component.dart';
/// ([x], [y]), rotate around its center with angle [angle].
///
/// It also uses the [anchor] property to properly position itself.
///
/// A [PositionComponent] can have children. The children are all updated and
/// rendered automatically when this is updated and rendered.
/// They are translated by this component's (x,y). They do not need to fit
/// within this component's (width, height).
abstract class PositionComponent extends Component {
double x = 0.0, y = 0.0, angle = 0.0;
double width = 0.0, height = 0.0;
/// X position of this component on the screen (measured from the top left corner).
double x = 0.0;
/// Y position of this component on the screen (measured from the top left corner).
double y = 0.0;
/// Angle (with respect to the x-axis) this component should be rendered with.
/// It is rotated around its anchor.
double angle = 0.0;
/// Width (size) that this component is rendered with.
/// This is not necessarily the source width of the asset.
double width = 0.0;
/// Height (size) that this component is rendered with.
/// This is not necessarily the source height of the asset.
double height = 0.0;
/// Anchor point for this component. This is where flame "grabs it".
/// The [x], [y] coordinates are relative to this point inside the component.
/// The [angle] is rotated around this point.
Anchor anchor = Anchor.topLeft;
/// Whether this component should be flipped on the X axis before being rendered.
bool renderFlipX = false;
/// Whether this component should be flipped ofn the Y axis before being rendered.
bool renderFlipY = false;
/// This is set by the BaseGame to tell this component to render additional debug information,
/// like borders, coordinates, etc.
/// This is very helpful while debbuging. Set your BaseGame debugMode to true.
/// You can also manually override this for certain components in order to identify issues.
bool debugMode = false;
final List<PositionComponentEffect> _effects = [];
final OrderedSet<Component> _children =
OrderedSet(Comparing.on((c) => c.priority()));
Color get debugColor => const Color(0xFFFF00FF);
@ -45,8 +84,22 @@ abstract class PositionComponent extends Component {
height = size.y;
}
Rect toRect() => Rect.fromLTWH(x - anchor.relativePosition.dx * width,
y - anchor.relativePosition.dy * height, width, height);
/// Returns the size of this component starting at (0, 0).
/// Effectively this is it's position with respect to itself.
/// Use this if the canvas is already translated by (x, y).
Rect toOriginRect() => Rect.fromLTWH(0, 0, width, height);
/// Returns the relative position/size of this component.
/// Relative because it might be translated by their parents (which is not considered here).
Rect toRect() => Rect.fromLTWH(
x - anchor.relativePosition.dx * width,
y - anchor.relativePosition.dy * height,
width,
height,
);
/// Mutates x, y, width and height using the provided [rect] as basis.
/// This is a relative rect, same definition that [toRect] use (therefore both methods are compatible, i.e. setByRect ∘ toRect = identity).
void setByRect(Rect rect) {
x = rect.left + anchor.relativePosition.dx * rect.width;
y = rect.top + anchor.relativePosition.dy * rect.height;
@ -59,14 +112,14 @@ abstract class PositionComponent extends Component {
}
double distance(PositionComponent c) {
return sqrt(pow(y - c.y, 2) + pow(x - c.x, 2));
return c.toPosition().distance(toPosition());
}
void renderDebugMode(Canvas canvas) {
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), _debugPaint);
canvas.drawRect(toOriginRect(), _debugPaint);
debugTextConfig.render(
canvas,
"x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}",
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
Position(-50, -15));
final Rect rect = toRect();
@ -74,11 +127,11 @@ abstract class PositionComponent extends Component {
final dy = rect.bottom;
debugTextConfig.render(
canvas,
"x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}",
'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
Position(width - 50, height));
}
void prepareCanvas(Canvas canvas) {
void _prepareCanvas(Canvas canvas) {
canvas.translate(x, y);
canvas.rotate(angle);
@ -92,10 +145,6 @@ abstract class PositionComponent extends Component {
canvas.scale(renderFlipX ? -1.0 : 1.0, renderFlipY ? -1.0 : 1.0);
canvas.translate(-width / 2, -height / 2);
}
if (debugMode) {
renderDebugMode(canvas);
}
}
void addEffect(PositionComponentEffect effect) {
@ -116,4 +165,80 @@ abstract class PositionComponent extends Component {
_effects.forEach((e) => e.update(dt));
_effects.removeWhere((e) => e.hasFinished());
}
/// This function recursively propagates an action to every children and grandchildren (and so on) of this component,
/// by keeping track of their positions by composing the positions of their parents.
/// For example, if this has a child that itself has a child, this will invoke handler for this (with no translation),
/// for the first child translating by this, and for the grand child by translating both this and the first child.
/// This is important to be used by the engine to propagate actions like rendering, taps, etc, but you can call it
/// yourself if you need to apply an action to the whole component chain.
/// It will only consider components of type T in the hierarchy, so use T = PositionComponent to target everything.
void propagateToChildren<T extends PositionComponent>(
void Function(T, Rect) handler,
) {
final rect = toRect();
if (this is T) {
handler(this as T, rect);
}
_children.forEach((c) {
if (c is PositionComponent) {
final newRect = c.toRect().translate(rect.left, rect.top);
if (c is T) {
handler(this as T, newRect);
}
c.propagateToChildren(handler);
}
});
}
@mustCallSuper
@override
void resize(Size size) {
super.resize(size);
_children.forEach((child) => child.resize(size));
}
@mustCallSuper
@override
void render(Canvas canvas) {
_prepareCanvas(canvas);
if (debugMode) {
renderDebugMode(canvas);
}
canvas.save();
_children.forEach((comp) => _renderChild(canvas, comp));
canvas.restore();
}
void _renderChild(Canvas canvas, Component c) {
if (!c.loaded()) {
return;
}
c.render(canvas);
canvas.restore();
canvas.save();
}
void addChild(Game gameRef, Component c) {
if (gameRef is BaseGame) {
gameRef.preAdd(c);
}
_children.add(c);
}
bool removeChild(PositionComponent c) {
return _children.remove(c);
}
Iterable<PositionComponent> removeChildren(
bool Function(PositionComponent) test,
) {
return _children.removeWhere(test);
}
void clearChildren() {
_children.clear();
}
}

View File

@ -1,5 +1,7 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import '../sprite_animation.dart';
import 'position_component.dart';
@ -55,9 +57,10 @@ class SpriteAnimationComponent extends PositionComponent {
@override
bool destroy() => destroyOnFinish && animation.isLastFrame;
@mustCallSuper
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
super.render(canvas);
animation.getSprite().render(
canvas,
width: width,

View File

@ -1,5 +1,7 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import '../sprite.dart';
import 'component.dart';
import 'position_component.dart';
@ -8,9 +10,14 @@ import 'position_component.dart';
/// position, scaled to have the designated size and rotated to the specified
/// angle.
///
/// This a commonly used child of [Component].
/// This a commonly used subclass of [Component].
class SpriteComponent extends PositionComponent {
/// The [sprite] to be rendered by this component.
Sprite sprite;
/// Use this to override the colour used (to apply tint or opacity).
///
/// If not provided the default is full white (no tint).
Paint overridePaint;
SpriteComponent();
@ -26,15 +33,20 @@ class SpriteComponent extends PositionComponent {
this.height = height;
}
@mustCallSuper
@override
void render(Canvas canvas) {
prepareCanvas(canvas);
sprite.render(canvas,
width: width, height: height, overridePaint: overridePaint);
super.render(canvas);
sprite.render(
canvas,
width: width,
height: height,
overridePaint: overridePaint,
);
}
@override
bool loaded() {
return sprite != null && sprite.loaded() && x != null && y != null;
return sprite?.loaded() == true && x != null && y != null;
}
}

View File

@ -142,14 +142,13 @@ class TextBoxComponent extends PositionComponent with Resizable {
if (_cache == null) {
return;
}
prepareCanvas(c);
super.render(c);
c.drawImage(_cache, Offset.zero, _imagePaint);
}
Future<Image> _redrawCache() {
final PictureRecorder recorder = PictureRecorder();
final Canvas c = Canvas(
recorder, Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()));
final Canvas c = Canvas(recorder, toOriginRect());
_fullRender(c);
return recorder.endRecording().toImage(width.toInt(), height.toInt());
}

View File

@ -1,5 +1,6 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import '../text_config.dart';
@ -38,9 +39,10 @@ class TextComponent extends PositionComponent {
height = _tp.height;
}
@mustCallSuper
@override
void render(Canvas c) {
prepareCanvas(c);
super.render(c);
_tp.paint(c, Offset.zero);
}
}

View File

@ -9,7 +9,6 @@ import 'package:ordered_set/comparing.dart';
import 'package:ordered_set/ordered_set.dart';
import '../components/component.dart';
import '../components/composed_component.dart';
import '../components/mixins/has_game_ref.dart';
import '../components/mixins/tapable.dart';
import '../components/position_component.dart';
@ -64,10 +63,6 @@ class BaseGame extends Game with FPSCounter {
c.resize(size);
}
if (c is ComposedComponent) {
c.components.forEach(preAdd);
}
c.onMount();
}

View File

@ -21,14 +21,17 @@ class EmbeddedGameWidget extends LeafRenderObjectWidget {
@override
RenderBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
child: GameRenderBox(context, game),
additionalConstraints:
BoxConstraints.expand(width: size?.x, height: size?.y));
child: GameRenderBox(context, game),
additionalConstraints:
BoxConstraints.expand(width: size?.x, height: size?.y),
);
}
@override
void updateRenderObject(
BuildContext context, RenderConstrainedBox renderBox) {
BuildContext context,
RenderConstrainedBox renderBox,
) {
renderBox
..child = GameRenderBox(context, game)
..additionalConstraints =

View File

@ -258,9 +258,9 @@ class _OverlayGameWidgetState extends State<OverlayGameWidget> {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child:
Stack(children: [widget.gameChild, ..._overlays.values.toList()]));
textDirection: TextDirection.ltr,
child: Stack(children: [widget.gameChild, ..._overlays.values.toList()]),
);
}
}
@ -272,6 +272,9 @@ class OverlayWidgetBuilder extends WidgetBuilder {
final container = super.build(game);
return OverlayGameWidget(
gameChild: container, game: game, key: UniqueKey());
gameChild: container,
game: game,
key: UniqueKey(),
);
}
}

View File

@ -70,8 +70,9 @@ class _AnimationWidget extends State<SpriteAnimationWidget>
widget.animation.reset();
_lastUpdated = DateTime.now().millisecond.toDouble();
_controller.repeat(
// Approximately 60 fps
period: const Duration(milliseconds: 16));
// Approximately 60 fps
period: const Duration(milliseconds: 16),
);
});
}

View File

@ -37,49 +37,80 @@ class _Painter extends widgets.CustomPainter {
final horizontalWidget = size.width - destTileSize * 2;
final verticalHeight = size.height - destTileSize * 2;
void render(Sprite sprite, double x, double y, double w, double h) {
sprite.renderRect(canvas, Rect.fromLTWH(x, y, w, h));
}
// Middle
middle.renderRect(
canvas,
Rect.fromLTWH(
destTileSize,
destTileSize,
horizontalWidget,
verticalHeight,
));
render(
middle,
destTileSize,
destTileSize,
horizontalWidget,
verticalHeight,
);
// Top and bottom side
topSide.renderRect(
canvas, Rect.fromLTWH(destTileSize, 0, horizontalWidget, destTileSize));
bottomSide.renderRect(
canvas,
Rect.fromLTWH(destTileSize, size.height - destTileSize,
horizontalWidget, destTileSize));
render(
topSide,
destTileSize,
0,
horizontalWidget,
destTileSize,
);
render(
bottomSide,
destTileSize,
size.height - destTileSize,
horizontalWidget,
destTileSize,
);
// Left and right side
leftSide.renderRect(
canvas, Rect.fromLTWH(0, destTileSize, destTileSize, verticalHeight));
rightSide.renderRect(
canvas,
Rect.fromLTWH(size.width - destTileSize, destTileSize, destTileSize,
verticalHeight));
render(
leftSide,
0,
destTileSize,
destTileSize,
verticalHeight,
);
render(
rightSide,
size.width - destTileSize,
destTileSize,
destTileSize,
verticalHeight,
);
// Corners
topLeftCorner.renderRect(
canvas, Rect.fromLTWH(0, 0, destTileSize, destTileSize));
topRightCorner.renderRect(
canvas,
Rect.fromLTWH(
size.width - destTileSize, 0, destTileSize, destTileSize));
bottomLeftCorner.renderRect(
canvas,
Rect.fromLTWH(
0, size.height - destTileSize, destTileSize, destTileSize));
bottomRightCorner.renderRect(
canvas,
Rect.fromLTWH(size.width - destTileSize, size.height - destTileSize,
destTileSize, destTileSize));
render(
topLeftCorner,
0,
0,
destTileSize,
destTileSize,
);
render(
topRightCorner,
size.width - destTileSize,
0,
destTileSize,
destTileSize,
);
render(
bottomLeftCorner,
0,
size.height - destTileSize,
destTileSize,
destTileSize,
);
render(
bottomRightCorner,
size.width - destTileSize,
size.height - destTileSize,
destTileSize,
destTileSize,
);
}
@override

View File

@ -1,6 +1,5 @@
import 'dart:ui';
import 'package:flame/components/composed_component.dart';
import 'package:flame/components/position_component.dart';
import 'package:flame/components/mixins/has_game_ref.dart';
import 'package:flame/components/mixins/resizable.dart';
@ -9,50 +8,26 @@ import 'package:flame/game/base_game.dart';
import 'package:flutter/gestures.dart';
import 'package:test/test.dart';
import 'package:flame/components/component.dart';
class MyGame extends BaseGame with HasTapableComponents {}
class MyTap extends PositionComponent with Tapable, Resizable {
bool tapped = false;
@override
void render(Canvas c) {}
@override
void update(double t) {
super.update(t);
}
@override
void onTapDown(TapDownDetails details) {
tapped = true;
}
@override
bool checkTapOverlap(Offset o) => true;
bool checkTapOverlap(Rect c, Offset o) => true;
}
class MyComposed extends Component with HasGameRef, Tapable, ComposedComponent {
@override
void update(double dt) {}
@override
void render(Canvas c) {}
class MyComposed extends PositionComponent with HasGameRef, Tapable {
@override
Rect toRect() => Rect.zero;
}
class PositionComponentNoNeedForRect extends PositionComponent with Tapable {
@override
void render(Canvas c) {}
@override
void update(double t) {
super.update(t);
}
}
class PositionComponentNoNeedForRect extends PositionComponent with Tapable {}
const Size size = Size(1.0, 1.0);
@ -61,7 +36,7 @@ void main() {
test('taps and resizes children', () {
final MyGame game = MyGame();
final MyTap child = MyTap();
final MyComposed wrapper = MyComposed()..add(child);
final MyComposed wrapper = MyComposed()..addChild(game, child);
game.size = size;
game.add(wrapper);

View File

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:flame/components/mixins/has_game_ref.dart';
import 'package:flame/game/base_game.dart';
import 'package:test/test.dart';
@ -14,9 +12,6 @@ class MyGame extends BaseGame {
}
class MyComponent extends PositionComponent with HasGameRef<MyGame> {
@override
void render(Canvas c) {}
void foo() {
gameRef.foo();
}

View File

@ -14,9 +14,6 @@ class MyComponent extends PositionComponent with Resizable {
@override
Iterable<Resizable> resizableChildren() => myChildren;
@override
void render(Canvas c) {}
}
class MyGame extends BaseGame {}