mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
Merge pull request #440 from flame-engine/luan.composed-p3
[v1] Part 3: Great Composed Component Refactor
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ class Square extends PositionComponent {
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.drawRect(toRect(), _paint);
|
||||
super.render(canvas);
|
||||
canvas.drawRect(toOriginRect(), _paint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -14,9 +14,6 @@ class MyComponent extends PositionComponent with Resizable {
|
||||
|
||||
@override
|
||||
Iterable<Resizable> resizableChildren() => myChildren;
|
||||
|
||||
@override
|
||||
void render(Canvas c) {}
|
||||
}
|
||||
|
||||
class MyGame extends BaseGame {}
|
||||
|
||||
Reference in New Issue
Block a user