Scale for PositionComponent (#892)

* Draft of PositionComponent.scale

* Use matrix transformations

* Update tests to take matrix transform into consideration

* Add tests for collision detection with scale

* Rename ScaleEffect to SizeEffect

* Use transform matrix to prepare canvas

* Fix scaledSizeCache

* Add changelog entries and docs

* Dartdoc on public access methods

* Update packages/flame/CHANGELOG.md

Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net>

* Move cache classes to own directory

Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net>
This commit is contained in:
Lukas Klingsbo
2021-08-06 21:59:52 +02:00
committed by GitHub
parent 4860cac87f
commit 54fbd260bc
31 changed files with 497 additions and 150 deletions

View File

@ -35,17 +35,18 @@ the value you give it no matter where it started.
When an effect is completed the callback `onComplete` will be called, it can be set as an optional When an effect is completed the callback `onComplete` will be called, it can be set as an optional
argument to your effect. argument to your effect.
## Common for MoveEffect, ScaleEffect and RotateEffect (SimplePositionComponentEffects) ## Common for MoveEffect, ScaleEffect, SizeEffect and RotateEffect (SimplePositionComponentEffects)
A common thing for `MoveEffect`, `ScaleEffect` and `RotateEffect` is that it takes `duration` and A common thing for `MoveEffect`, `ScaleEffect`, `SizeEffect` and `RotateEffect` is that it takes
`speed` as arguments, but only use one of them at a time. `duration` and `speed` as arguments, but only use one of them at a time.
- Duration means the time it takes for one iteration from beginning to end, with alternation taken - Duration means the time it takes for one iteration from beginning to end, with alternation taken
into account (but not `isInfinite`). into account (but not `isInfinite`).
- Speed is the speed of the effect - Speed is the speed of the effect
+ pixels per second for `MoveEffect` + pixels per second for `MoveEffect`
+ pixels per second for `ScaleEffect` + pixels per second for `SizeEffect`
+ radians per second for `RotateEffect` + radians per second for `RotateEffect`
+ percentage/100 per second for `ScaleEffect`
One of these two needs to be defined, if both are defined `duration` takes precedence. One of these two needs to be defined, if both are defined `duration` takes precedence.
@ -93,6 +94,29 @@ component will first move to `(120, 0)` and then to `(120, 100)`.
## ScaleEffect ## ScaleEffect
Applied to `PositionComponent`s, this effect can be used to change the scale with which the
component and its children is rendered on the canvas with, using an
[animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html).
This also affects the `scaledSize` property of the component.
The speed is measured in percentage/100 per second, and remember that you can give `duration` as an
argument instead of `speed`.
Usage example:
```dart
import 'package:flame/effects.dart';
// Square is a PositionComponent
square.addEffect(ScaleEffect(
scale: Vector2.all(2.0),
speed: 1.0,
curve: Curves.bounceInOut,
));
```
## SizeEffect
Applied to `PositionComponent`s, this effect can be used to change the width and height of the Applied to `PositionComponent`s, this effect can be used to change the width and height of the
component, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html). component, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html).
@ -104,8 +128,8 @@ Usage example:
import 'package:flame/effects.dart'; import 'package:flame/effects.dart';
// Square is a PositionComponent // Square is a PositionComponent
square.addEffect(ScaleEffect( square.addEffect(SizeEffect(
size: Size(300, 300), size: Vector2.all(300),
speed: 250.0, speed: 250.0,
curve: Curves.bounceInOut, curve: Curves.bounceInOut,
)); ));
@ -153,7 +177,7 @@ You can make the sequence go in a loop by setting both `isInfinite: true` and `i
Usage example: Usage example:
```dart ```dart
final sequence = SequenceEffect( final sequence = SequenceEffect(
effects: [move1, scale, move2, rotate], effects: [move1, size, move2, rotate],
isInfinite: true, isInfinite: true,
isAlternating: true); isAlternating: true);
myComponent.addEffect(sequence); myComponent.addEffect(sequence);
@ -179,7 +203,7 @@ You can make the combined effect go in a loop by setting both `isInfinite: true`
Usage example: Usage example:
```dart ```dart
final combination = CombinedEffect( final combination = CombinedEffect(
effects: [move, scale, rotate], effects: [move, size, rotate],
isInfinite: true, isInfinite: true,
isAlternating: true); isAlternating: true);
myComponent.addEffect(combination); myComponent.addEffect(combination);

View File

@ -50,12 +50,12 @@ abstract class MyCollidable extends PositionComponent
angleDelta = dt * rotationSpeed; angleDelta = dt * rotationSpeed;
angle = (angle + angleDelta) % (2 * pi); angle = (angle + angleDelta) % (2 * pi);
// Takes rotation into consideration (which topLeftPosition doesn't) // Takes rotation into consideration (which topLeftPosition doesn't)
final topLeft = absoluteCenter - (size / 2); final topLeft = absoluteCenter - (scaledSize / 2);
if (topLeft.x + size.x < 0 || if (topLeft.x + scaledSize.x < 0 ||
topLeft.y + size.y < 0 || topLeft.y + scaledSize.y < 0 ||
topLeft.x > screenCollidable.size.x || topLeft.x > screenCollidable.scaledSize.x ||
topLeft.y > screenCollidable.size.y) { topLeft.y > screenCollidable.scaledSize.y) {
final moduloSize = screenCollidable.size + size; final moduloSize = screenCollidable.scaledSize + scaledSize;
topLeftPosition = topLeftPosition % moduloSize; topLeftPosition = topLeftPosition % moduloSize;
} }
} }
@ -65,7 +65,7 @@ abstract class MyCollidable extends PositionComponent
super.render(canvas); super.render(canvas);
renderHitboxes(canvas, paint: _activePaint); renderHitboxes(canvas, paint: _activePaint);
if (_isDragged) { if (_isDragged) {
final localCenter = (size / 2).toOffset(); final localCenter = (scaledSize / 2).toOffset();
canvas.drawCircle(localCenter, 5, _activePaint); canvas.drawCircle(localCenter, 5, _activePaint);
} }
} }

View File

@ -1,12 +1,14 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/input.dart';
class Square extends PositionComponent { class Square extends PositionComponent {
Square(Vector2 position, Vector2 size, {double angle = 0}) { Square(Vector2 position, Vector2 size, {double angle = 0})
this.position.setFrom(position); : super(
this.size.setFrom(size); position: position,
this.angle = angle; size: size,
} angle: angle,
);
} }
class ParentSquare extends Square with HasGameRef { class ParentSquare extends Square with HasGameRef {
@ -31,7 +33,7 @@ class ParentSquare extends Square with HasGameRef {
} }
} }
class Composability extends BaseGame { class Composability extends BaseGame with TapDetector {
late ParentSquare _parent; late ParentSquare _parent;
@override @override
@ -49,4 +51,10 @@ class Composability extends BaseGame {
super.update(dt); super.update(dt);
_parent.angle += dt; _parent.angle += dt;
} }
@override
void onTap() {
super.onTap();
_parent.scale = Vector2.all(2.0);
}
} }

View File

@ -41,7 +41,7 @@ class CombinedEffectGame extends BaseGame with TapDetector {
curve: Curves.bounceInOut, curve: Curves.bounceInOut,
); );
final scale = ScaleEffect( final scale = SizeEffect(
size: currentTap, size: currentTap,
speed: 200.0, speed: 200.0,
curve: Curves.linear, curve: Curves.linear,

View File

@ -10,9 +10,15 @@ import 'opacity_effect.dart';
import 'rotate_effect.dart'; import 'rotate_effect.dart';
import 'scale_effect.dart'; import 'scale_effect.dart';
import 'sequence_effect.dart'; import 'sequence_effect.dart';
import 'size_effect.dart';
void addEffectsStories(Dashbook dashbook) { void addEffectsStories(Dashbook dashbook) {
dashbook.storiesOf('Effects') dashbook.storiesOf('Effects')
..add(
'Size Effect',
(_) => GameWidget(game: SizeEffectGame()),
codeLink: baseLink('effects/size_effect.dart'),
)
..add( ..add(
'Scale Effect', 'Scale Effect',
(_) => GameWidget(game: ScaleEffectGame()), (_) => GameWidget(game: ScaleEffectGame()),

View File

@ -49,7 +49,7 @@ class InfiniteEffectGame extends BaseGame with TapDetector {
); );
redSquare.addEffect( redSquare.addEffect(
ScaleEffect( SizeEffect(
size: p, size: p,
speed: 250.0, speed: 250.0,
curve: Curves.easeInCubic, curve: Curves.easeInCubic,

View File

@ -3,6 +3,7 @@ import 'package:flame/effects.dart';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../commons/square_component.dart'; import '../../commons/square_component.dart';
@ -16,19 +17,26 @@ class ScaleEffectGame extends BaseGame with TapDetector {
square = SquareComponent() square = SquareComponent()
..position.setValues(200, 200) ..position.setValues(200, 200)
..anchor = Anchor.center; ..anchor = Anchor.center;
square.paint = BasicPalette.white.paint()..style = PaintingStyle.stroke;
final childSquare = SquareComponent()
..position = Vector2.all(70)
..size = Vector2.all(20)
..anchor = Anchor.center;
square.addChild(childSquare);
add(square); add(square);
} }
@override @override
void onTap() { void onTap() {
final s = grow ? 300.0 : 100.0; final s = grow ? 3.0 : 1.0;
grow = !grow; grow = !grow;
square.addEffect( square.addEffect(
ScaleEffect( ScaleEffect(
size: Vector2.all(s), scale: Vector2.all(s),
speed: 250.0, speed: 2.0,
curve: Curves.bounceInOut, curve: Curves.linear,
), ),
); );
} }

View File

@ -41,7 +41,7 @@ class SequenceEffectGame extends BaseGame with TapDetector {
curve: Curves.easeIn, curve: Curves.easeIn,
); );
final scale = ScaleEffect( final scale = SizeEffect(
size: currentTap, size: currentTap,
speed: 100.0, speed: 100.0,
curve: Curves.easeInCubic, curve: Curves.easeInCubic,

View File

@ -0,0 +1,35 @@
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/extensions.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flutter/material.dart';
import '../../commons/square_component.dart';
class SizeEffectGame extends BaseGame with TapDetector {
late SquareComponent square;
bool grow = true;
@override
Future<void> onLoad() async {
square = SquareComponent()
..position.setValues(200, 200)
..anchor = Anchor.center;
add(square);
}
@override
void onTap() {
final s = grow ? 300.0 : 100.0;
grow = !grow;
square.addEffect(
SizeEffect(
size: Vector2.all(s),
speed: 250.0,
curve: Curves.bounceInOut,
),
);
}
}

View File

@ -24,6 +24,9 @@
- Adding `SpriteGroupComponent` - Adding `SpriteGroupComponent`
- Fix truncated last frame in non-looping animations - Fix truncated last frame in non-looping animations
- Default size of `SpriteComponent` is `srcSize` instead of spritesheet size - Default size of `SpriteComponent` is `srcSize` instead of spritesheet size
- Rename `ScaleEffect` to `SizeEffect`
- Introduce `scale` on `PositionComponent`
- Add `ScaleEffect` that works on `scale` instead of `size`
## [1.0.0-releasecandidate.13] ## [1.0.0-releasecandidate.13]
- Fix camera not ending up in the correct position on long jumps - Fix camera not ending up in the correct position on long jumps

View File

@ -5,5 +5,5 @@ export 'src/effects/move_effect.dart';
export 'src/effects/rotate_effect.dart'; export 'src/effects/rotate_effect.dart';
export 'src/effects/scale_effect.dart'; export 'src/effects/scale_effect.dart';
export 'src/effects/sequence_effect.dart'; export 'src/effects/sequence_effect.dart';
export 'src/effects/size_effect.dart';
export 'src/extensions/vector2.dart'; export 'src/extensions/vector2.dart';

View File

@ -0,0 +1,28 @@
/// Used for caching calculated values, the cache is determined to be valid by
/// comparing a list of values that can be of any type and is compared to the
/// values that was last used when the cache was updated.
class ValueCache<T> {
T? value;
List<dynamic> _lastValidCacheValues = <dynamic>[];
ValueCache();
bool isCacheValid<F>(List<F> validCacheValues) {
if (value == null) {
return false;
}
for (var i = 0; i < _lastValidCacheValues.length; ++i) {
if (_lastValidCacheValues[i] != validCacheValues[i]) {
return false;
}
}
return true;
}
T updateCache<F>(T value, List<F> validCacheValues) {
this.value = value;
_lastValidCacheValues = validCacheValues;
return value;
}
}

View File

@ -36,25 +36,26 @@ class HudMarginComponent<T extends BaseGame> extends PositionComponent
if (margin != null) { if (margin != null) {
final margin = this.margin!; final margin = this.margin!;
final x = margin.left != 0 final x = margin.left != 0
? margin.left + size.x / 2 ? margin.left + scaledSize.x / 2
: gameRef.viewport.effectiveSize.x - margin.right - size.x / 2; : gameRef.viewport.effectiveSize.x - margin.right - scaledSize.x / 2;
final y = margin.top != 0 final y = margin.top != 0
? margin.top + size.y / 2 ? margin.top + scaledSize.y / 2
: gameRef.viewport.effectiveSize.y - margin.bottom - size.y / 2; : gameRef.viewport.effectiveSize.y - margin.bottom - scaledSize.y / 2;
position.setValues(x, y); position.setValues(x, y);
position = Anchor.center.toOtherAnchorPosition(center, anchor, size); position =
Anchor.center.toOtherAnchorPosition(center, anchor, scaledSize);
} else { } else {
final topLeft = gameRef.viewport.effectiveSize - final topLeft = gameRef.viewport.effectiveSize -
anchor.toOtherAnchorPosition( anchor.toOtherAnchorPosition(
position, position,
Anchor.topLeft, Anchor.topLeft,
size, scaledSize,
); );
final bottomRight = gameRef.viewport.effectiveSize - final bottomRight = gameRef.viewport.effectiveSize -
anchor.toOtherAnchorPosition( anchor.toOtherAnchorPosition(
position, position,
Anchor.bottomRight, Anchor.bottomRight,
size, scaledSize,
); );
margin = EdgeInsets.fromLTRB( margin = EdgeInsets.fromLTRB(
topLeft.x, topLeft.x,

View File

@ -37,7 +37,7 @@ mixin Hitbox on PositionComponent {
/// check can be done first to see if it even is possible that the shapes can /// check can be done first to see if it even is possible that the shapes can
/// overlap, since the shapes have to be within the size of the component. /// overlap, since the shapes have to be within the size of the component.
bool possiblyOverlapping(Hitbox other) { bool possiblyOverlapping(Hitbox other) {
final maxDistance = other.size.length + size.length; final maxDistance = other.scaledSize.length + scaledSize.length;
return other.absoluteCenter.distanceToSquared(absoluteCenter) <= return other.absoluteCenter.distanceToSquared(absoluteCenter) <=
maxDistance * maxDistance; maxDistance * maxDistance;
} }
@ -47,6 +47,6 @@ mixin Hitbox on PositionComponent {
/// contain the point, since the shapes have to be within the size of the /// contain the point, since the shapes have to be within the size of the
/// component. /// component.
bool possiblyContainsPoint(Vector2 point) { bool possiblyContainsPoint(Vector2 point) {
return absoluteCenter.distanceToSquared(point) <= size.length2; return absoluteCenter.distanceToSquared(point) <= scaledSize.length2;
} }
} }

View File

@ -6,6 +6,7 @@ import '../extensions/rect.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import '../geometry/rectangle.dart'; import '../geometry/rectangle.dart';
import 'base_component.dart'; import 'base_component.dart';
import 'cache/value_cache.dart';
import 'component.dart'; import 'component.dart';
import 'mixins/hitbox.dart'; import 'mixins/hitbox.dart';
@ -23,9 +24,9 @@ import 'mixins/hitbox.dart';
/// within this component's (width, height). /// within this component's (width, height).
abstract class PositionComponent extends BaseComponent { abstract class PositionComponent extends BaseComponent {
/// The position of this component on the screen (relative to the anchor). /// The position of this component on the screen (relative to the anchor).
final Vector2 _position;
Vector2 get position => _position; Vector2 get position => _position;
set position(Vector2 position) => _position.setFrom(position); set position(Vector2 position) => _position.setFrom(position);
final Vector2 _position;
/// X position of this component on the screen (relative to the anchor). /// X position of this component on the screen (relative to the anchor).
double get x => _position.x; double get x => _position.x;
@ -35,19 +36,38 @@ abstract class PositionComponent extends BaseComponent {
double get y => _position.y; double get y => _position.y;
set y(double y) => _position.y = y; set y(double y) => _position.y = y;
/// The size that this component is rendered with. /// The size that this component is rendered with before [scale] is applied.
/// This is not necessarily the source size of the asset. /// This is not necessarily the source size of the asset.
final Vector2 _size;
Vector2 get size => _size; Vector2 get size => _size;
set size(Vector2 size) => _size.setFrom(size); set size(Vector2 size) => _size.setFrom(size);
final Vector2 _size;
/// Width (size) that this component is rendered with. /// Width (size) that this component is rendered with.
double get width => size.x; double get width => scaledSize.x;
set width(double width) => size.x = width; set width(double width) => size.x = width / scale.x;
/// Height (size) that this component is rendered with. /// Height (size) that this component is rendered with.
double get height => size.y; double get height => scaledSize.y;
set height(double height) => size.y = height; set height(double height) => size.y = height / scale.y;
/// The scale factor of this component
Vector2 get scale => _scale;
set scale(Vector2 scale) => _scale.setFrom(scale);
final Vector2 _scale;
/// Cache to store the calculated scaled size
final ValueCache<Vector2> _scaledSizeCache = ValueCache();
/// The size that this component is rendered with after [scale] is applied.
Vector2 get scaledSize {
if (!_scaledSizeCache.isCacheValid([scale, size])) {
_scaledSizeCache.updateCache(
size.clone()..multiply(scale),
[scale.clone(), size.clone()],
);
}
return _scaledSizeCache.value!;
}
/// Get the absolute position, with the anchor taken into consideration /// Get the absolute position, with the anchor taken into consideration
Vector2 get absolutePosition => absoluteParentPosition + position; Vector2 get absolutePosition => absoluteParentPosition + position;
@ -57,13 +77,13 @@ abstract class PositionComponent extends BaseComponent {
return anchor.toOtherAnchorPosition( return anchor.toOtherAnchorPosition(
position, position,
Anchor.topLeft, Anchor.topLeft,
size, scaledSize,
); );
} }
/// Set the top left position regardless of the anchor /// Set the top left position regardless of the anchor
set topLeftPosition(Vector2 position) { set topLeftPosition(Vector2 position) {
this.position = position + (anchor.toVector2()..multiply(size)); this.position = position + (anchor.toVector2()..multiply(scaledSize));
} }
/// Get the absolute top left position regardless of whether it is a child or not /// Get the absolute top left position regardless of whether it is a child or not
@ -93,7 +113,7 @@ abstract class PositionComponent extends BaseComponent {
if (anchor == Anchor.center) { if (anchor == Anchor.center) {
return position; return position;
} else { } else {
return anchor.toOtherAnchorPosition(position, Anchor.center, size) return anchor.toOtherAnchorPosition(position, Anchor.center, scaledSize)
..rotate(angle, center: absolutePosition); ..rotate(angle, center: absolutePosition);
} }
} }
@ -116,25 +136,10 @@ abstract class PositionComponent extends BaseComponent {
/// Whether this component should be flipped ofn the Y axis before being rendered. /// Whether this component should be flipped ofn the Y axis before being rendered.
bool renderFlipY = false; bool renderFlipY = false;
/// Returns the relative position/size of this component.
/// Relative because it might be translated by their parents (which is not considered here).
Rect toRect() => topLeftPosition.toPositionedRect(size);
/// Returns the absolute position/size of this component.
/// Absolute because it takes any possible parent position into consideration.
Rect toAbsoluteRect() => absoluteTopLeftPosition.toPositionedRect(size);
/// Mutates position and size 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) {
size.setValues(rect.width, rect.height);
topLeftPosition = rect.topLeft.toVector2();
}
PositionComponent({ PositionComponent({
Vector2? position, Vector2? position,
Vector2? size, Vector2? size,
Vector2? scale,
this.angle = 0.0, this.angle = 0.0,
this.anchor = Anchor.topLeft, this.anchor = Anchor.topLeft,
this.renderFlipX = false, this.renderFlipX = false,
@ -142,6 +147,7 @@ abstract class PositionComponent extends BaseComponent {
int? priority, int? priority,
}) : _position = position ?? Vector2.zero(), }) : _position = position ?? Vector2.zero(),
_size = size ?? Vector2.zero(), _size = size ?? Vector2.zero(),
_scale = scale ?? Vector2.all(1.0),
super(priority: priority); super(priority: priority);
@override @override
@ -160,7 +166,7 @@ abstract class PositionComponent extends BaseComponent {
if (this is Hitbox) { if (this is Hitbox) {
(this as Hitbox).renderHitboxes(canvas); (this as Hitbox).renderHitboxes(canvas);
} }
canvas.drawRect(size.toRect(), debugPaint); canvas.drawRect(scaledSize.toRect(), debugPaint);
debugTextPaint.render( debugTextPaint.render(
canvas, canvas,
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}', 'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
@ -177,19 +183,51 @@ abstract class PositionComponent extends BaseComponent {
); );
} }
final Matrix4 _preRenderMatrix = Matrix4.identity();
@override @override
void preRender(Canvas canvas) { void preRender(Canvas canvas) {
canvas.translate(x, y); // Move canvas to the components anchor position.
canvas.rotate(angle); _preRenderMatrix.translate(x, y);
final delta = -anchor.toVector2() // Rotate canvas around anchor
..multiply(size); _preRenderMatrix.rotateZ(angle);
canvas.translate(delta.x, delta.y); // Scale canvas if it should be scaled
if (scale.x != 1.0 || scale.y != 1.0) {
_preRenderMatrix.scale(scale.x, scale.y);
}
canvas.transform(_preRenderMatrix.storage);
_preRenderMatrix.setIdentity();
final delta = anchor.toVector2()..multiply(scaledSize);
canvas.translate(-delta.x, -delta.y);
// Handle inverted rendering by moving center and flipping. // Handle inverted rendering by moving center and flipping.
if (renderFlipX || renderFlipY) { if (renderFlipX || renderFlipY) {
canvas.translate(width / 2, height / 2); final size = scaledSize;
canvas.scale(renderFlipX ? -1.0 : 1.0, renderFlipY ? -1.0 : 1.0); _preRenderMatrix.translate(size.x / 2, size.y / 2);
canvas.translate(-width / 2, -height / 2); _preRenderMatrix.scale(
renderFlipX ? -1.0 : 1.0,
renderFlipY ? -1.0 : 1.0,
);
canvas.transform(_preRenderMatrix.storage);
canvas.translate(-size.x / 2, -size.y / 2);
_preRenderMatrix.setIdentity();
} }
} }
/// Returns the relative position/size of this component.
/// Relative because it might be translated by their parents (which is not considered here).
Rect toRect() => topLeftPosition.toPositionedRect(scaledSize);
/// Returns the absolute position/size of this component.
/// Absolute because it takes any possible parent position into consideration.
Rect toAbsoluteRect() => absoluteTopLeftPosition.toPositionedRect(scaledSize);
/// Mutates position and size 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) {
size.setValues(rect.width, rect.height);
topLeftPosition = rect.topLeft.toVector2();
}
} }

View File

@ -8,8 +8,8 @@ export './color_effect.dart';
export './move_effect.dart'; export './move_effect.dart';
export './opacity_effect.dart'; export './opacity_effect.dart';
export './rotate_effect.dart'; export './rotate_effect.dart';
export './scale_effect.dart';
export './sequence_effect.dart'; export './sequence_effect.dart';
export './size_effect.dart';
abstract class ComponentEffect<T extends BaseComponent> { abstract class ComponentEffect<T extends BaseComponent> {
T? component; T? component;
@ -140,16 +140,19 @@ abstract class PositionComponentEffect
Vector2? originalPosition; Vector2? originalPosition;
double? originalAngle; double? originalAngle;
Vector2? originalSize; Vector2? originalSize;
Vector2? originalScale;
/// Used to be able to determine the end state of a sequence of effects /// Used to be able to determine the end state of a sequence of effects
Vector2? endPosition; Vector2? endPosition;
double? endAngle; double? endAngle;
Vector2? endSize; Vector2? endSize;
Vector2? endScale;
/// Whether the state of a certain field was modified by the effect /// Whether the state of a certain field was modified by the effect
final bool modifiesPosition; final bool modifiesPosition;
final bool modifiesAngle; final bool modifiesAngle;
final bool modifiesSize; final bool modifiesSize;
final bool modifiesScale;
PositionComponentEffect( PositionComponentEffect(
bool initialIsInfinite, bool initialIsInfinite,
@ -159,6 +162,7 @@ abstract class PositionComponentEffect
this.modifiesPosition = false, this.modifiesPosition = false,
this.modifiesAngle = false, this.modifiesAngle = false,
this.modifiesSize = false, this.modifiesSize = false,
this.modifiesScale = false,
VoidCallback? onComplete, VoidCallback? onComplete,
}) : super( }) : super(
initialIsInfinite, initialIsInfinite,
@ -176,6 +180,7 @@ abstract class PositionComponentEffect
originalPosition = component.position.clone(); originalPosition = component.position.clone();
originalAngle = component.angle; originalAngle = component.angle;
originalSize = component.size.clone(); originalSize = component.size.clone();
originalScale = component.scale.clone();
/// If these aren't modified by the extending effect it is assumed that the /// If these aren't modified by the extending effect it is assumed that the
/// effect didn't bring the component to another state than the one it /// effect didn't bring the component to another state than the one it
@ -183,12 +188,18 @@ abstract class PositionComponentEffect
endPosition = component.position.clone(); endPosition = component.position.clone();
endAngle = component.angle; endAngle = component.angle;
endSize = component.size.clone(); endSize = component.size.clone();
endScale = component.scale.clone();
} }
/// Only change the parts of the component that is affected by the /// Only change the parts of the component that is affected by the
/// effect, and only set the state if it is the root effect (not part of /// effect, and only set the state if it is the root effect (not part of
/// another effect, like children of a CombinedEffect or SequenceEffect). /// another effect, like children of a CombinedEffect or SequenceEffect).
void _setComponentState(Vector2? position, double? angle, Vector2? size) { void _setComponentState(
Vector2? position,
double? angle,
Vector2? size,
Vector2? scale,
) {
if (isRootEffect()) { if (isRootEffect()) {
if (modifiesPosition) { if (modifiesPosition) {
assert( assert(
@ -211,17 +222,29 @@ abstract class PositionComponentEffect
); );
component?.size.setFrom(size!); component?.size.setFrom(size!);
} }
if (modifiesScale) {
assert(
scale != null,
'`scale` must not be `null` for an effect which modifies `scale`',
);
component?.scale.setFrom(scale!);
}
} }
} }
@override @override
void setComponentToOriginalState() { void setComponentToOriginalState() {
_setComponentState(originalPosition, originalAngle, originalSize); _setComponentState(
originalPosition,
originalAngle,
originalSize,
originalScale,
);
} }
@override @override
void setComponentToEndState() { void setComponentToEndState() {
_setComponentState(endPosition, endAngle, endSize); _setComponentState(endPosition, endAngle, endSize, endScale);
} }
} }
@ -239,6 +262,7 @@ abstract class SimplePositionComponentEffect extends PositionComponentEffect {
bool modifiesPosition = false, bool modifiesPosition = false,
bool modifiesAngle = false, bool modifiesAngle = false,
bool modifiesSize = false, bool modifiesSize = false,
bool modifiesScale = false,
VoidCallback? onComplete, VoidCallback? onComplete,
}) : assert( }) : assert(
(duration != null) ^ (speed != null), (duration != null) ^ (speed != null),
@ -252,6 +276,7 @@ abstract class SimplePositionComponentEffect extends PositionComponentEffect {
modifiesPosition: modifiesPosition, modifiesPosition: modifiesPosition,
modifiesAngle: modifiesAngle, modifiesAngle: modifiesAngle,
modifiesSize: modifiesSize, modifiesSize: modifiesSize,
modifiesScale: modifiesScale,
onComplete: onComplete, onComplete: onComplete,
); );
} }

View File

@ -3,8 +3,8 @@ import 'effects.dart';
export './move_effect.dart'; export './move_effect.dart';
export './rotate_effect.dart'; export './rotate_effect.dart';
export './scale_effect.dart';
export './sequence_effect.dart'; export './sequence_effect.dart';
export './size_effect.dart';
class EffectsHandler { class EffectsHandler {
/// The effects that should run on the component /// The effects that should run on the component

View File

@ -7,13 +7,13 @@ import '../extensions/vector2.dart';
import 'effects.dart'; import 'effects.dart';
class ScaleEffect extends SimplePositionComponentEffect { class ScaleEffect extends SimplePositionComponentEffect {
Vector2 size; Vector2 scale;
late Vector2 _startSize; late Vector2 _startScale;
late Vector2 _delta; late Vector2 _delta;
/// Duration or speed needs to be defined /// Duration or speed needs to be defined
ScaleEffect({ ScaleEffect({
required this.size, required this.scale,
double? duration, // How long it should take for completion double? duration, // How long it should take for completion
double? speed, // The speed of the scaling in pixels per second double? speed, // The speed of the scaling in pixels per second
Curve? curve, Curve? curve,
@ -32,17 +32,17 @@ class ScaleEffect extends SimplePositionComponentEffect {
speed: speed, speed: speed,
curve: curve, curve: curve,
isRelative: isRelative, isRelative: isRelative,
modifiesSize: true, modifiesScale: true,
onComplete: onComplete, onComplete: onComplete,
); );
@override @override
void initialize(PositionComponent component) { void initialize(PositionComponent component) {
super.initialize(component); super.initialize(component);
_startSize = component.size.clone(); _startScale = component.scale.clone();
_delta = isRelative ? size : size - _startSize; _delta = isRelative ? scale : scale - _startScale;
if (!isAlternating) { if (!isAlternating) {
endSize = _startSize + _delta; endScale = _startScale + _delta;
} }
speed ??= _delta.length / duration!; speed ??= _delta.length / duration!;
duration ??= _delta.length / speed!; duration ??= _delta.length / speed!;
@ -52,6 +52,6 @@ class ScaleEffect extends SimplePositionComponentEffect {
@override @override
void update(double dt) { void update(double dt) {
super.update(dt); super.update(dt);
component?.size.setFrom(_startSize + _delta * curveProgress); component?.scale.setFrom(_startScale + _delta * curveProgress);
} }
} }

View File

@ -0,0 +1,57 @@
import 'dart:ui';
import 'package:flutter/animation.dart';
import '../../components.dart';
import '../extensions/vector2.dart';
import 'effects.dart';
class SizeEffect extends SimplePositionComponentEffect {
Vector2 size;
late Vector2 _startSize;
late Vector2 _delta;
/// Duration or speed needs to be defined
SizeEffect({
required this.size,
double? duration, // How long it should take for completion
double? speed, // The speed of the scaling in pixels per second
Curve? curve,
bool isInfinite = false,
bool isAlternating = false,
bool isRelative = false,
VoidCallback? onComplete,
}) : assert(
duration != null || speed != null,
'Either speed or duration necessary',
),
super(
isInfinite,
isAlternating,
duration: duration,
speed: speed,
curve: curve,
isRelative: isRelative,
modifiesSize: true,
onComplete: onComplete,
);
@override
void initialize(PositionComponent component) {
super.initialize(component);
_startSize = component.size.clone();
_delta = isRelative ? size : size - _startSize;
if (!isAlternating) {
endSize = _startSize + _delta;
}
speed ??= _delta.length / duration!;
duration ??= _delta.length / speed!;
peakTime = isAlternating ? duration! / 2 : duration!;
}
@override
void update(double dt) {
super.update(dt);
component?.size.setFrom(_startSize + _delta * curveProgress);
}
}

View File

@ -3,6 +3,7 @@ import 'dart:ui' hide Canvas;
import '../../game.dart'; import '../../game.dart';
import '../../geometry.dart'; import '../../geometry.dart';
import '../components/cache/value_cache.dart';
import '../extensions/canvas.dart'; import '../extensions/canvas.dart';
import '../extensions/rect.dart'; import '../extensions/rect.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
@ -68,7 +69,7 @@ class Polygon extends Shape {
normalizedVertices.map((_) => Vector2.zero()).toList(growable: false); normalizedVertices.map((_) => Vector2.zero()).toList(growable: false);
} }
final _cachedScaledShape = ShapeCache<Iterable<Vector2>>(); final _cachedScaledShape = ValueCache<Iterable<Vector2>>();
/// Gives back the shape vectors multiplied by the size /// Gives back the shape vectors multiplied by the size
Iterable<Vector2> scaled() { Iterable<Vector2> scaled() {
@ -82,7 +83,7 @@ class Polygon extends Shape {
return _cachedScaledShape.value!; return _cachedScaledShape.value!;
} }
final _cachedRenderPath = ShapeCache<Path>(); final _cachedRenderPath = ValueCache<Path>();
@override @override
void render(Canvas canvas, Paint paint) { void render(Canvas canvas, Paint paint) {
@ -114,7 +115,7 @@ class Polygon extends Shape {
canvas.drawPath(_cachedRenderPath.value!, paint); canvas.drawPath(_cachedRenderPath.value!, paint);
} }
final _cachedHitbox = ShapeCache<List<Vector2>>(); final _cachedHitbox = ValueCache<List<Vector2>>();
/// Gives back the vertices represented as a list of points which /// Gives back the vertices represented as a list of points which
/// are the "corners" of the hitbox rotated with [angle]. /// are the "corners" of the hitbox rotated with [angle].

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import '../../components.dart'; import '../../components.dart';
import '../../game.dart'; import '../../game.dart';
import '../../palette.dart'; import '../../palette.dart';
import '../components/cache/value_cache.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import 'shape_intersections.dart' as intersection_system; import 'shape_intersections.dart' as intersection_system;
@ -11,9 +12,9 @@ import 'shape_intersections.dart' as intersection_system;
/// center. /// center.
/// A point can be determined to be within of outside of a shape. /// A point can be determined to be within of outside of a shape.
abstract class Shape { abstract class Shape {
final ShapeCache<Vector2> _halfSizeCache = ShapeCache(); final ValueCache<Vector2> _halfSizeCache = ValueCache();
final ShapeCache<Vector2> _localCenterCache = ShapeCache(); final ValueCache<Vector2> _localCenterCache = ValueCache();
final ShapeCache<Vector2> _absoluteCenterCache = ShapeCache(); final ValueCache<Vector2> _absoluteCenterCache = ValueCache();
/// Should be the center of that [offsetPosition] and [relativeOffset] /// Should be the center of that [offsetPosition] and [relativeOffset]
/// should be calculated from, if they are not set this is the center of the /// should be calculated from, if they are not set this is the center of the
@ -133,7 +134,7 @@ mixin HitboxShape on Shape {
late PositionComponent component; late PositionComponent component;
@override @override
Vector2 get size => component.size; Vector2 get size => component.scaledSize;
@override @override
double get parentAngle => component.angle; double get parentAngle => component.angle;
@ -159,32 +160,3 @@ typedef CollisionEndCallback = void Function(HitboxShape other);
void emptyCollisionCallback(Set<Vector2> _, HitboxShape __) {} void emptyCollisionCallback(Set<Vector2> _, HitboxShape __) {}
void emptyCollisionEndCallback(HitboxShape _) {} void emptyCollisionEndCallback(HitboxShape _) {}
/// Used for caching calculated shapes, the cache is determined to be valid by
/// comparing a list of values that can be of any type and is compared to the
/// values that was last used when the cache was updated.
class ShapeCache<T> {
T? value;
List<dynamic> _lastValidCacheValues = <dynamic>[];
ShapeCache();
bool isCacheValid<F>(List<F> validCacheValues) {
if (value == null) {
return false;
}
for (var i = 0; i < _lastValidCacheValues.length; ++i) {
if (_lastValidCacheValues[i] != validCacheValues[i]) {
return false;
}
}
return true;
}
T updateCache<F>(T value, List<F> validCacheValues) {
this.value = value;
_lastValidCacheValues = validCacheValues;
return value;
}
}

View File

@ -3,10 +3,10 @@ import 'dart:ui';
import 'package:flutter/material.dart' as material; import 'package:flutter/material.dart' as material;
import 'anchor.dart'; import 'anchor.dart';
import 'components/cache/memory_cache.dart';
import 'components/text_component.dart'; import 'components/text_component.dart';
import 'extensions/size.dart'; import 'extensions/size.dart';
import 'extensions/vector2.dart'; import 'extensions/vector2.dart';
import 'memory_cache.dart';
/// [TextRenderer] is the abstract API that Flame uses for rendering text in its features /// [TextRenderer] is the abstract API that Flame uses for rendering text in its features
/// this class can be extended to provide an implementation of text rendering in the engine. /// this class can be extended to provide an implementation of text rendering in the engine.

View File

@ -256,6 +256,41 @@ void main() {
true, true,
); );
}); });
test('Detects collision after scale', () {
final blockA = TestBlock(
Vector2.zero(),
Vector2.all(10),
CollidableType.active,
);
final blockB = TestBlock(
Vector2.all(11),
Vector2.all(10),
CollidableType.active,
);
final game = gameWithCollidables([blockA, blockB]);
expect(blockA.collidedWith(blockB), false);
expect(blockB.collidedWith(blockA), false);
expect(blockA.collisions.length, 0);
expect(blockB.collisions.length, 0);
blockA.scale = Vector2.all(2.0);
game.update(0);
expect(blockA.collidedWith(blockB), true);
expect(blockB.collidedWith(blockA), true);
expect(blockA.collisions.length, 1);
expect(blockB.collisions.length, 1);
});
test('TestPoint detects point after scale', () {
final blockA = TestBlock(
Vector2.zero(),
Vector2.all(10),
CollidableType.active,
);
final game = gameWithCollidables([blockA]);
expect(blockA.containsPoint(Vector2.all(11)), false);
blockA.scale = Vector2.all(2.0);
game.update(0);
expect(blockA.containsPoint(Vector2.all(11)), true);
});
}, },
); );
} }

View File

@ -27,7 +27,7 @@ void main() {
bool isAlternating = false, bool isAlternating = false,
bool hasAlternatingMoveEffect = false, bool hasAlternatingMoveEffect = false,
bool hasAlternatingRotateEffect = false, bool hasAlternatingRotateEffect = false,
bool hasAlternatingScaleEffect = false, bool hasAlternatingSizeEffect = false,
}) { }) {
final move = MoveEffect( final move = MoveEffect(
path: path, path: path,
@ -39,10 +39,10 @@ void main() {
duration: randomDuration(), duration: randomDuration(),
isAlternating: hasAlternatingRotateEffect, isAlternating: hasAlternatingRotateEffect,
)..skipEffectReset = true; )..skipEffectReset = true;
final scale = ScaleEffect( final scale = SizeEffect(
size: argumentSize, size: argumentSize,
duration: randomDuration(), duration: randomDuration(),
isAlternating: hasAlternatingScaleEffect, isAlternating: hasAlternatingSizeEffect,
)..skipEffectReset = true; )..skipEffectReset = true;
return CombinedEffect( return CombinedEffect(
effects: [move, scale, rotate], effects: [move, scale, rotate],
@ -169,13 +169,13 @@ void main() {
); );
testWidgets( testWidgets(
'CombinedEffect can contain alternating ScaleEffect', 'CombinedEffect can contain alternating SizeEffect',
(WidgetTester tester) async { (WidgetTester tester) async {
final PositionComponent positionComponent = component(); final PositionComponent positionComponent = component();
effectTest( effectTest(
tester, tester,
positionComponent, positionComponent,
effect(hasAlternatingScaleEffect: true), effect(hasAlternatingSizeEffect: true),
expectedPosition: path.last, expectedPosition: path.last,
expectedAngle: argumentAngle, expectedAngle: argumentAngle,
expectedSize: positionComponent.size.clone(), expectedSize: positionComponent.size.clone(),

View File

@ -25,9 +25,11 @@ void effectTest(
double expectedAngle = 0.0, double expectedAngle = 0.0,
Vector2? expectedPosition, Vector2? expectedPosition,
Vector2? expectedSize, Vector2? expectedSize,
Vector2? expectedScale,
}) async { }) async {
expectedPosition ??= Vector2.zero(); expectedPosition ??= Vector2.zero();
expectedSize ??= Vector2.all(100.0); expectedSize ??= Vector2.all(100.0);
expectedScale ??= Vector2.all(1.0);
final callback = Callback(); final callback = Callback();
effect.onComplete = callback.call; effect.onComplete = callback.call;
final game = BaseGame(); final game = BaseGame();
@ -63,6 +65,11 @@ void effectTest(
expectedSize, expectedSize,
reason: 'Size is not correct', reason: 'Size is not correct',
); );
expectVector2(
component.scale,
expectedScale,
reason: 'Scale is not correct',
);
} else { } else {
// To account for float number operations making effects not finish // To account for float number operations making effects not finish
const epsilon = 0.001; const epsilon = 0.001;
@ -87,6 +94,11 @@ void effectTest(
expectedSize, expectedSize,
reason: 'Size is not exactly correct', reason: 'Size is not exactly correct',
); );
expectVector2(
component.scale,
expectedScale,
reason: 'Scale is not exactly correct',
);
} }
expect(effect.hasCompleted(), shouldComplete, reason: 'Effect shouldFinish'); expect(effect.hasCompleted(), shouldComplete, reason: 'Effect shouldFinish');
game.update(0.0); game.update(0.0);
@ -103,11 +115,13 @@ class TestComponent extends PositionComponent {
TestComponent({ TestComponent({
Vector2? position, Vector2? position,
Vector2? size, Vector2? size,
Vector2? scale,
double angle = 0.0, double angle = 0.0,
Anchor anchor = Anchor.center, Anchor anchor = Anchor.center,
}) : super( }) : super(
position: position, position: position,
size: size ?? Vector2.all(100.0), size: size ?? Vector2.all(100.0),
scale: scale,
angle: angle, angle: angle,
anchor: anchor, anchor: anchor,
); );

View File

@ -9,12 +9,12 @@ import 'effect_test_utils.dart';
void main() { void main() {
final random = Random(); final random = Random();
Vector2 randomVector2() => (Vector2.random(random) * 100)..round(); Vector2 randomVector2() => (Vector2.random(random) * 100)..round();
final argumentSize = randomVector2(); final argumentScale = randomVector2();
TestComponent component() => TestComponent(size: randomVector2()); TestComponent component() => TestComponent(scale: randomVector2());
ScaleEffect effect({bool isInfinite = false, bool isAlternating = false}) { ScaleEffect effect({bool isInfinite = false, bool isAlternating = false}) {
return ScaleEffect( return ScaleEffect(
size: argumentSize, scale: argumentScale,
duration: 1 + random.nextInt(100).toDouble(), duration: 1 + random.nextInt(100).toDouble(),
isInfinite: isInfinite, isInfinite: isInfinite,
isAlternating: isAlternating, isAlternating: isAlternating,
@ -26,7 +26,7 @@ void main() {
tester, tester,
component(), component(),
effect(), effect(),
expectedSize: argumentSize, expectedScale: argumentScale,
); );
}); });
@ -37,7 +37,7 @@ void main() {
tester, tester,
component(), component(),
effect(), effect(),
expectedSize: argumentSize, expectedScale: argumentScale,
iterations: 1.5, iterations: 1.5,
); );
}, },
@ -49,7 +49,7 @@ void main() {
tester, tester,
positionComponent, positionComponent,
effect(isAlternating: true), effect(isAlternating: true),
expectedSize: positionComponent.size.clone(), expectedScale: positionComponent.scale.clone(),
); );
}); });
@ -61,7 +61,7 @@ void main() {
tester, tester,
positionComponent, positionComponent,
effect(isInfinite: true, isAlternating: true), effect(isInfinite: true, isAlternating: true),
expectedSize: positionComponent.size.clone(), expectedScale: positionComponent.scale.clone(),
shouldComplete: false, shouldComplete: false,
); );
}, },
@ -73,7 +73,7 @@ void main() {
tester, tester,
positionComponent, positionComponent,
effect(isAlternating: true), effect(isAlternating: true),
expectedSize: argumentSize, expectedScale: argumentScale,
shouldComplete: false, shouldComplete: false,
iterations: 0.5, iterations: 0.5,
); );
@ -85,7 +85,7 @@ void main() {
tester, tester,
positionComponent, positionComponent,
effect(isInfinite: true), effect(isInfinite: true),
expectedSize: argumentSize, expectedScale: argumentScale,
iterations: 3.0, iterations: 3.0,
shouldComplete: false, shouldComplete: false,
); );

View File

@ -28,7 +28,7 @@ void main() {
bool isAlternating = false, bool isAlternating = false,
bool hasAlternatingMoveEffect = false, bool hasAlternatingMoveEffect = false,
bool hasAlternatingRotateEffect = false, bool hasAlternatingRotateEffect = false,
bool hasAlternatingScaleEffect = false, bool hasAlternatingSizeEffect = false,
}) { }) {
final move = MoveEffect( final move = MoveEffect(
path: path, path: path,
@ -40,10 +40,10 @@ void main() {
duration: randomDuration(), duration: randomDuration(),
isAlternating: hasAlternatingRotateEffect, isAlternating: hasAlternatingRotateEffect,
)..skipEffectReset = true; )..skipEffectReset = true;
final scale = ScaleEffect( final scale = SizeEffect(
size: argumentSize, size: argumentSize,
duration: randomDuration(), duration: randomDuration(),
isAlternating: hasAlternatingScaleEffect, isAlternating: hasAlternatingSizeEffect,
)..skipEffectReset = true; )..skipEffectReset = true;
return SequenceEffect( return SequenceEffect(
effects: [move, scale, rotate], effects: [move, scale, rotate],
@ -169,13 +169,13 @@ void main() {
); );
testWidgets( testWidgets(
'SequenceEffect can contain alternating ScaleEffect', 'SequenceEffect can contain alternating SizeEffect',
(WidgetTester tester) async { (WidgetTester tester) async {
final PositionComponent positionComponent = component(); final PositionComponent positionComponent = component();
effectTest( effectTest(
tester, tester,
positionComponent, positionComponent,
effect(hasAlternatingScaleEffect: true), effect(hasAlternatingSizeEffect: true),
expectedPosition: path.last, expectedPosition: path.last,
expectedAngle: argumentAngle, expectedAngle: argumentAngle,
expectedSize: positionComponent.size.clone(), expectedSize: positionComponent.size.clone(),

View File

@ -0,0 +1,93 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter_test/flutter_test.dart';
import 'effect_test_utils.dart';
void main() {
final random = Random();
Vector2 randomVector2() => (Vector2.random(random) * 100)..round();
final argumentSize = randomVector2();
TestComponent component() => TestComponent(size: randomVector2());
SizeEffect effect({bool isInfinite = false, bool isAlternating = false}) {
return SizeEffect(
size: argumentSize,
duration: 1 + random.nextInt(100).toDouble(),
isInfinite: isInfinite,
isAlternating: isAlternating,
)..skipEffectReset = true;
}
testWidgets('SizeEffect can scale', (WidgetTester tester) async {
effectTest(
tester,
component(),
effect(),
expectedSize: argumentSize,
);
});
testWidgets(
'SizeEffect will stop scaling after it is done',
(WidgetTester tester) async {
effectTest(
tester,
component(),
effect(),
expectedSize: argumentSize,
iterations: 1.5,
);
},
);
testWidgets('SizeEffect can alternate', (WidgetTester tester) async {
final PositionComponent positionComponent = component();
effectTest(
tester,
positionComponent,
effect(isAlternating: true),
expectedSize: positionComponent.size.clone(),
);
});
testWidgets(
'SizeEffect can alternate and be infinite',
(WidgetTester tester) async {
final PositionComponent positionComponent = component();
effectTest(
tester,
positionComponent,
effect(isInfinite: true, isAlternating: true),
expectedSize: positionComponent.size.clone(),
shouldComplete: false,
);
},
);
testWidgets('SizeEffect alternation can peak', (WidgetTester tester) async {
final PositionComponent positionComponent = component();
effectTest(
tester,
positionComponent,
effect(isAlternating: true),
expectedSize: argumentSize,
shouldComplete: false,
iterations: 0.5,
);
});
testWidgets('SizeEffect can be infinite', (WidgetTester tester) async {
final PositionComponent positionComponent = component();
effectTest(
tester,
positionComponent,
effect(isInfinite: true),
expectedSize: argumentSize,
iterations: 3.0,
shouldComplete: false,
);
});
}

View File

@ -127,7 +127,7 @@ void main() {
), ),
[ [
'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)', // camera translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)', // camera translation
'translate(10.0, 10.0)', // position component translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 10.0, 10.0, 0.0, 1.0)', // position component translate
'translate(0.0, 0.0)', // position component anchor 'translate(0.0, 0.0)', // position component anchor
], ],
); );
@ -155,7 +155,7 @@ void main() {
), ),
[ [
'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -4.0, -4.0, 0.0, 1.0)', // camera translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -4.0, -4.0, 0.0, 1.0)', // camera translation
'translate(10.0, 10.0)', // position component translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 10.0, 10.0, 0.0, 1.0)', // position component translate
'translate(0.0, 0.0)', // position component anchor 'translate(0.0, 0.0)', // position component anchor
], ],
); );
@ -200,7 +200,7 @@ void main() {
), ),
[ [
'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 40.0, 30.0, 0.0, 1.0)', // camera translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 40.0, 30.0, 0.0, 1.0)', // camera translation
'translate(10.0, 20.0)', // position component translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 10.0, 20.0, 0.0, 1.0)', // position component translate
'translate(-0.5, -0.5)', // position component anchor 'translate(-0.5, -0.5)', // position component anchor
], ],
// result: 50 - w/2, 50 - h/2 (perfectly centered) // result: 50 - w/2, 50 - h/2 (perfectly centered)
@ -231,7 +231,7 @@ void main() {
), ),
[ [
'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -550.0, -1920.0, 0.0, 1.0)', // camera translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -550.0, -1920.0, 0.0, 1.0)', // camera translation
'translate(600.0, 2000.0)', // position component translation 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 600.0, 2000.0, 0.0, 1.0)', // position component translation
'translate(-0.5, -0.5)', // position component anchor 'translate(-0.5, -0.5)', // position component anchor
], ],
// result: 50 - w/2, 80 - h/2 (respects fractional relative offset) // result: 50 - w/2, 80 - h/2 (respects fractional relative offset)
@ -323,7 +323,7 @@ void main() {
), ),
[ [
'transform(2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0)', // camera translation and zoom 'transform(2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0)', // camera translation and zoom
'translate(100.0, 100.0)', // position component 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 100.0, 100.0, 0.0, 1.0)', // position component translate
'translate(-0.5, -0.5)', // anchor 'translate(-0.5, -0.5)', // anchor
], ],
); );
@ -346,7 +346,7 @@ void main() {
), ),
[ [
'transform(2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 100.0, 100.0, 0.0, 1.0)', // camera translation and zoom 'transform(2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 100.0, 100.0, 0.0, 1.0)', // camera translation and zoom
'translate(100.0, 100.0)', // position component 'transform(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 100.0, 100.0, 0.0, 1.0)', // position component translate
'translate(-0.5, -0.5)', // anchor 'translate(-0.5, -0.5)', // anchor
], ],
); );

View File

@ -1,7 +1,6 @@
import 'package:flame/src/components/cache/memory_cache.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:flame/src/memory_cache.dart';
void main() { void main() {
group('MemoryCache', () { group('MemoryCache', () {
test('basic cache addition', () { test('basic cache addition', () {