mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
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:
@ -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
|
||||
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
|
||||
`speed` as arguments, but only use one of them at a time.
|
||||
A common thing for `MoveEffect`, `ScaleEffect`, `SizeEffect` and `RotateEffect` is that it takes
|
||||
`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
|
||||
into account (but not `isInfinite`).
|
||||
- Speed is the speed of the effect
|
||||
+ pixels per second for `MoveEffect`
|
||||
+ pixels per second for `ScaleEffect`
|
||||
+ pixels per second for `SizeEffect`
|
||||
+ 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.
|
||||
|
||||
@ -93,6 +94,29 @@ component will first move to `(120, 0)` and then to `(120, 100)`.
|
||||
|
||||
## 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
|
||||
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';
|
||||
|
||||
// Square is a PositionComponent
|
||||
square.addEffect(ScaleEffect(
|
||||
size: Size(300, 300),
|
||||
square.addEffect(SizeEffect(
|
||||
size: Vector2.all(300),
|
||||
speed: 250.0,
|
||||
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:
|
||||
```dart
|
||||
final sequence = SequenceEffect(
|
||||
effects: [move1, scale, move2, rotate],
|
||||
effects: [move1, size, move2, rotate],
|
||||
isInfinite: true,
|
||||
isAlternating: true);
|
||||
myComponent.addEffect(sequence);
|
||||
@ -179,7 +203,7 @@ You can make the combined effect go in a loop by setting both `isInfinite: true`
|
||||
Usage example:
|
||||
```dart
|
||||
final combination = CombinedEffect(
|
||||
effects: [move, scale, rotate],
|
||||
effects: [move, size, rotate],
|
||||
isInfinite: true,
|
||||
isAlternating: true);
|
||||
myComponent.addEffect(combination);
|
||||
|
||||
@ -50,12 +50,12 @@ abstract class MyCollidable extends PositionComponent
|
||||
angleDelta = dt * rotationSpeed;
|
||||
angle = (angle + angleDelta) % (2 * pi);
|
||||
// Takes rotation into consideration (which topLeftPosition doesn't)
|
||||
final topLeft = absoluteCenter - (size / 2);
|
||||
if (topLeft.x + size.x < 0 ||
|
||||
topLeft.y + size.y < 0 ||
|
||||
topLeft.x > screenCollidable.size.x ||
|
||||
topLeft.y > screenCollidable.size.y) {
|
||||
final moduloSize = screenCollidable.size + size;
|
||||
final topLeft = absoluteCenter - (scaledSize / 2);
|
||||
if (topLeft.x + scaledSize.x < 0 ||
|
||||
topLeft.y + scaledSize.y < 0 ||
|
||||
topLeft.x > screenCollidable.scaledSize.x ||
|
||||
topLeft.y > screenCollidable.scaledSize.y) {
|
||||
final moduloSize = screenCollidable.scaledSize + scaledSize;
|
||||
topLeftPosition = topLeftPosition % moduloSize;
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ abstract class MyCollidable extends PositionComponent
|
||||
super.render(canvas);
|
||||
renderHitboxes(canvas, paint: _activePaint);
|
||||
if (_isDragged) {
|
||||
final localCenter = (size / 2).toOffset();
|
||||
final localCenter = (scaledSize / 2).toOffset();
|
||||
canvas.drawCircle(localCenter, 5, _activePaint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
|
||||
class Square extends PositionComponent {
|
||||
Square(Vector2 position, Vector2 size, {double angle = 0}) {
|
||||
this.position.setFrom(position);
|
||||
this.size.setFrom(size);
|
||||
this.angle = angle;
|
||||
}
|
||||
Square(Vector2 position, Vector2 size, {double angle = 0})
|
||||
: super(
|
||||
position: position,
|
||||
size: size,
|
||||
angle: angle,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
@ -49,4 +51,10 @@ class Composability extends BaseGame {
|
||||
super.update(dt);
|
||||
_parent.angle += dt;
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
super.onTap();
|
||||
_parent.scale = Vector2.all(2.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ class CombinedEffectGame extends BaseGame with TapDetector {
|
||||
curve: Curves.bounceInOut,
|
||||
);
|
||||
|
||||
final scale = ScaleEffect(
|
||||
final scale = SizeEffect(
|
||||
size: currentTap,
|
||||
speed: 200.0,
|
||||
curve: Curves.linear,
|
||||
|
||||
@ -10,9 +10,15 @@ import 'opacity_effect.dart';
|
||||
import 'rotate_effect.dart';
|
||||
import 'scale_effect.dart';
|
||||
import 'sequence_effect.dart';
|
||||
import 'size_effect.dart';
|
||||
|
||||
void addEffectsStories(Dashbook dashbook) {
|
||||
dashbook.storiesOf('Effects')
|
||||
..add(
|
||||
'Size Effect',
|
||||
(_) => GameWidget(game: SizeEffectGame()),
|
||||
codeLink: baseLink('effects/size_effect.dart'),
|
||||
)
|
||||
..add(
|
||||
'Scale Effect',
|
||||
(_) => GameWidget(game: ScaleEffectGame()),
|
||||
|
||||
@ -49,7 +49,7 @@ class InfiniteEffectGame extends BaseGame with TapDetector {
|
||||
);
|
||||
|
||||
redSquare.addEffect(
|
||||
ScaleEffect(
|
||||
SizeEffect(
|
||||
size: p,
|
||||
speed: 250.0,
|
||||
curve: Curves.easeInCubic,
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:flame/effects.dart';
|
||||
import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/input.dart';
|
||||
import 'package:flame/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../commons/square_component.dart';
|
||||
@ -16,19 +17,26 @@ class ScaleEffectGame extends BaseGame with TapDetector {
|
||||
square = SquareComponent()
|
||||
..position.setValues(200, 200)
|
||||
..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);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTap() {
|
||||
final s = grow ? 300.0 : 100.0;
|
||||
final s = grow ? 3.0 : 1.0;
|
||||
|
||||
grow = !grow;
|
||||
square.addEffect(
|
||||
ScaleEffect(
|
||||
size: Vector2.all(s),
|
||||
speed: 250.0,
|
||||
curve: Curves.bounceInOut,
|
||||
scale: Vector2.all(s),
|
||||
speed: 2.0,
|
||||
curve: Curves.linear,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ class SequenceEffectGame extends BaseGame with TapDetector {
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
|
||||
final scale = ScaleEffect(
|
||||
final scale = SizeEffect(
|
||||
size: currentTap,
|
||||
speed: 100.0,
|
||||
curve: Curves.easeInCubic,
|
||||
|
||||
35
examples/lib/stories/effects/size_effect.dart
Normal file
35
examples/lib/stories/effects/size_effect.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,9 @@
|
||||
- Adding `SpriteGroupComponent`
|
||||
- Fix truncated last frame in non-looping animations
|
||||
- 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]
|
||||
- Fix camera not ending up in the correct position on long jumps
|
||||
|
||||
@ -5,5 +5,5 @@ export 'src/effects/move_effect.dart';
|
||||
export 'src/effects/rotate_effect.dart';
|
||||
export 'src/effects/scale_effect.dart';
|
||||
export 'src/effects/sequence_effect.dart';
|
||||
|
||||
export 'src/effects/size_effect.dart';
|
||||
export 'src/extensions/vector2.dart';
|
||||
|
||||
28
packages/flame/lib/src/components/cache/value_cache.dart
vendored
Normal file
28
packages/flame/lib/src/components/cache/value_cache.dart
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -36,25 +36,26 @@ class HudMarginComponent<T extends BaseGame> extends PositionComponent
|
||||
if (margin != null) {
|
||||
final margin = this.margin!;
|
||||
final x = margin.left != 0
|
||||
? margin.left + size.x / 2
|
||||
: gameRef.viewport.effectiveSize.x - margin.right - size.x / 2;
|
||||
? margin.left + scaledSize.x / 2
|
||||
: gameRef.viewport.effectiveSize.x - margin.right - scaledSize.x / 2;
|
||||
final y = margin.top != 0
|
||||
? margin.top + size.y / 2
|
||||
: gameRef.viewport.effectiveSize.y - margin.bottom - size.y / 2;
|
||||
? margin.top + scaledSize.y / 2
|
||||
: gameRef.viewport.effectiveSize.y - margin.bottom - scaledSize.y / 2;
|
||||
position.setValues(x, y);
|
||||
position = Anchor.center.toOtherAnchorPosition(center, anchor, size);
|
||||
position =
|
||||
Anchor.center.toOtherAnchorPosition(center, anchor, scaledSize);
|
||||
} else {
|
||||
final topLeft = gameRef.viewport.effectiveSize -
|
||||
anchor.toOtherAnchorPosition(
|
||||
position,
|
||||
Anchor.topLeft,
|
||||
size,
|
||||
scaledSize,
|
||||
);
|
||||
final bottomRight = gameRef.viewport.effectiveSize -
|
||||
anchor.toOtherAnchorPosition(
|
||||
position,
|
||||
Anchor.bottomRight,
|
||||
size,
|
||||
scaledSize,
|
||||
);
|
||||
margin = EdgeInsets.fromLTRB(
|
||||
topLeft.x,
|
||||
|
||||
@ -37,7 +37,7 @@ mixin Hitbox on PositionComponent {
|
||||
/// 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.
|
||||
bool possiblyOverlapping(Hitbox other) {
|
||||
final maxDistance = other.size.length + size.length;
|
||||
final maxDistance = other.scaledSize.length + scaledSize.length;
|
||||
return other.absoluteCenter.distanceToSquared(absoluteCenter) <=
|
||||
maxDistance * maxDistance;
|
||||
}
|
||||
@ -47,6 +47,6 @@ mixin Hitbox on PositionComponent {
|
||||
/// contain the point, since the shapes have to be within the size of the
|
||||
/// component.
|
||||
bool possiblyContainsPoint(Vector2 point) {
|
||||
return absoluteCenter.distanceToSquared(point) <= size.length2;
|
||||
return absoluteCenter.distanceToSquared(point) <= scaledSize.length2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import '../extensions/rect.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import '../geometry/rectangle.dart';
|
||||
import 'base_component.dart';
|
||||
import 'cache/value_cache.dart';
|
||||
import 'component.dart';
|
||||
import 'mixins/hitbox.dart';
|
||||
|
||||
@ -23,9 +24,9 @@ import 'mixins/hitbox.dart';
|
||||
/// within this component's (width, height).
|
||||
abstract class PositionComponent extends BaseComponent {
|
||||
/// The position of this component on the screen (relative to the anchor).
|
||||
final Vector2 _position;
|
||||
Vector2 get position => _position;
|
||||
set position(Vector2 position) => _position.setFrom(position);
|
||||
final Vector2 _position;
|
||||
|
||||
/// X position of this component on the screen (relative to the anchor).
|
||||
double get x => _position.x;
|
||||
@ -35,19 +36,38 @@ abstract class PositionComponent extends BaseComponent {
|
||||
double get y => _position.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.
|
||||
final Vector2 _size;
|
||||
Vector2 get size => _size;
|
||||
set size(Vector2 size) => _size.setFrom(size);
|
||||
final Vector2 _size;
|
||||
|
||||
/// Width (size) that this component is rendered with.
|
||||
double get width => size.x;
|
||||
set width(double width) => size.x = width;
|
||||
double get width => scaledSize.x;
|
||||
set width(double width) => size.x = width / scale.x;
|
||||
|
||||
/// Height (size) that this component is rendered with.
|
||||
double get height => size.y;
|
||||
set height(double height) => size.y = height;
|
||||
double get height => scaledSize.y;
|
||||
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
|
||||
Vector2 get absolutePosition => absoluteParentPosition + position;
|
||||
@ -57,13 +77,13 @@ abstract class PositionComponent extends BaseComponent {
|
||||
return anchor.toOtherAnchorPosition(
|
||||
position,
|
||||
Anchor.topLeft,
|
||||
size,
|
||||
scaledSize,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the top left position regardless of the anchor
|
||||
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
|
||||
@ -93,7 +113,7 @@ abstract class PositionComponent extends BaseComponent {
|
||||
if (anchor == Anchor.center) {
|
||||
return position;
|
||||
} else {
|
||||
return anchor.toOtherAnchorPosition(position, Anchor.center, size)
|
||||
return anchor.toOtherAnchorPosition(position, Anchor.center, scaledSize)
|
||||
..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.
|
||||
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({
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
this.angle = 0.0,
|
||||
this.anchor = Anchor.topLeft,
|
||||
this.renderFlipX = false,
|
||||
@ -142,6 +147,7 @@ abstract class PositionComponent extends BaseComponent {
|
||||
int? priority,
|
||||
}) : _position = position ?? Vector2.zero(),
|
||||
_size = size ?? Vector2.zero(),
|
||||
_scale = scale ?? Vector2.all(1.0),
|
||||
super(priority: priority);
|
||||
|
||||
@override
|
||||
@ -160,7 +166,7 @@ abstract class PositionComponent extends BaseComponent {
|
||||
if (this is Hitbox) {
|
||||
(this as Hitbox).renderHitboxes(canvas);
|
||||
}
|
||||
canvas.drawRect(size.toRect(), debugPaint);
|
||||
canvas.drawRect(scaledSize.toRect(), debugPaint);
|
||||
debugTextPaint.render(
|
||||
canvas,
|
||||
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
|
||||
@ -177,19 +183,51 @@ abstract class PositionComponent extends BaseComponent {
|
||||
);
|
||||
}
|
||||
|
||||
final Matrix4 _preRenderMatrix = Matrix4.identity();
|
||||
|
||||
@override
|
||||
void preRender(Canvas canvas) {
|
||||
canvas.translate(x, y);
|
||||
canvas.rotate(angle);
|
||||
final delta = -anchor.toVector2()
|
||||
..multiply(size);
|
||||
canvas.translate(delta.x, delta.y);
|
||||
// Move canvas to the components anchor position.
|
||||
_preRenderMatrix.translate(x, y);
|
||||
// Rotate canvas around anchor
|
||||
_preRenderMatrix.rotateZ(angle);
|
||||
// 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.
|
||||
if (renderFlipX || renderFlipY) {
|
||||
canvas.translate(width / 2, height / 2);
|
||||
canvas.scale(renderFlipX ? -1.0 : 1.0, renderFlipY ? -1.0 : 1.0);
|
||||
canvas.translate(-width / 2, -height / 2);
|
||||
final size = scaledSize;
|
||||
_preRenderMatrix.translate(size.x / 2, size.y / 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ export './color_effect.dart';
|
||||
export './move_effect.dart';
|
||||
export './opacity_effect.dart';
|
||||
export './rotate_effect.dart';
|
||||
export './scale_effect.dart';
|
||||
export './sequence_effect.dart';
|
||||
export './size_effect.dart';
|
||||
|
||||
abstract class ComponentEffect<T extends BaseComponent> {
|
||||
T? component;
|
||||
@ -140,16 +140,19 @@ abstract class PositionComponentEffect
|
||||
Vector2? originalPosition;
|
||||
double? originalAngle;
|
||||
Vector2? originalSize;
|
||||
Vector2? originalScale;
|
||||
|
||||
/// Used to be able to determine the end state of a sequence of effects
|
||||
Vector2? endPosition;
|
||||
double? endAngle;
|
||||
Vector2? endSize;
|
||||
Vector2? endScale;
|
||||
|
||||
/// Whether the state of a certain field was modified by the effect
|
||||
final bool modifiesPosition;
|
||||
final bool modifiesAngle;
|
||||
final bool modifiesSize;
|
||||
final bool modifiesScale;
|
||||
|
||||
PositionComponentEffect(
|
||||
bool initialIsInfinite,
|
||||
@ -159,6 +162,7 @@ abstract class PositionComponentEffect
|
||||
this.modifiesPosition = false,
|
||||
this.modifiesAngle = false,
|
||||
this.modifiesSize = false,
|
||||
this.modifiesScale = false,
|
||||
VoidCallback? onComplete,
|
||||
}) : super(
|
||||
initialIsInfinite,
|
||||
@ -176,6 +180,7 @@ abstract class PositionComponentEffect
|
||||
originalPosition = component.position.clone();
|
||||
originalAngle = component.angle;
|
||||
originalSize = component.size.clone();
|
||||
originalScale = component.scale.clone();
|
||||
|
||||
/// 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
|
||||
@ -183,12 +188,18 @@ abstract class PositionComponentEffect
|
||||
endPosition = component.position.clone();
|
||||
endAngle = component.angle;
|
||||
endSize = component.size.clone();
|
||||
endScale = component.scale.clone();
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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 (modifiesPosition) {
|
||||
assert(
|
||||
@ -211,17 +222,29 @@ abstract class PositionComponentEffect
|
||||
);
|
||||
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
|
||||
void setComponentToOriginalState() {
|
||||
_setComponentState(originalPosition, originalAngle, originalSize);
|
||||
_setComponentState(
|
||||
originalPosition,
|
||||
originalAngle,
|
||||
originalSize,
|
||||
originalScale,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setComponentToEndState() {
|
||||
_setComponentState(endPosition, endAngle, endSize);
|
||||
_setComponentState(endPosition, endAngle, endSize, endScale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,6 +262,7 @@ abstract class SimplePositionComponentEffect extends PositionComponentEffect {
|
||||
bool modifiesPosition = false,
|
||||
bool modifiesAngle = false,
|
||||
bool modifiesSize = false,
|
||||
bool modifiesScale = false,
|
||||
VoidCallback? onComplete,
|
||||
}) : assert(
|
||||
(duration != null) ^ (speed != null),
|
||||
@ -252,6 +276,7 @@ abstract class SimplePositionComponentEffect extends PositionComponentEffect {
|
||||
modifiesPosition: modifiesPosition,
|
||||
modifiesAngle: modifiesAngle,
|
||||
modifiesSize: modifiesSize,
|
||||
modifiesScale: modifiesScale,
|
||||
onComplete: onComplete,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import 'effects.dart';
|
||||
|
||||
export './move_effect.dart';
|
||||
export './rotate_effect.dart';
|
||||
export './scale_effect.dart';
|
||||
export './sequence_effect.dart';
|
||||
export './size_effect.dart';
|
||||
|
||||
class EffectsHandler {
|
||||
/// The effects that should run on the component
|
||||
|
||||
@ -7,13 +7,13 @@ import '../extensions/vector2.dart';
|
||||
import 'effects.dart';
|
||||
|
||||
class ScaleEffect extends SimplePositionComponentEffect {
|
||||
Vector2 size;
|
||||
late Vector2 _startSize;
|
||||
Vector2 scale;
|
||||
late Vector2 _startScale;
|
||||
late Vector2 _delta;
|
||||
|
||||
/// Duration or speed needs to be defined
|
||||
ScaleEffect({
|
||||
required this.size,
|
||||
required this.scale,
|
||||
double? duration, // How long it should take for completion
|
||||
double? speed, // The speed of the scaling in pixels per second
|
||||
Curve? curve,
|
||||
@ -32,17 +32,17 @@ class ScaleEffect extends SimplePositionComponentEffect {
|
||||
speed: speed,
|
||||
curve: curve,
|
||||
isRelative: isRelative,
|
||||
modifiesSize: true,
|
||||
modifiesScale: true,
|
||||
onComplete: onComplete,
|
||||
);
|
||||
|
||||
@override
|
||||
void initialize(PositionComponent component) {
|
||||
super.initialize(component);
|
||||
_startSize = component.size.clone();
|
||||
_delta = isRelative ? size : size - _startSize;
|
||||
_startScale = component.scale.clone();
|
||||
_delta = isRelative ? scale : scale - _startScale;
|
||||
if (!isAlternating) {
|
||||
endSize = _startSize + _delta;
|
||||
endScale = _startScale + _delta;
|
||||
}
|
||||
speed ??= _delta.length / duration!;
|
||||
duration ??= _delta.length / speed!;
|
||||
@ -52,6 +52,6 @@ class ScaleEffect extends SimplePositionComponentEffect {
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
component?.size.setFrom(_startSize + _delta * curveProgress);
|
||||
component?.scale.setFrom(_startScale + _delta * curveProgress);
|
||||
}
|
||||
}
|
||||
|
||||
57
packages/flame/lib/src/effects/size_effect.dart
Normal file
57
packages/flame/lib/src/effects/size_effect.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@ import 'dart:ui' hide Canvas;
|
||||
|
||||
import '../../game.dart';
|
||||
import '../../geometry.dart';
|
||||
import '../components/cache/value_cache.dart';
|
||||
import '../extensions/canvas.dart';
|
||||
import '../extensions/rect.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
@ -68,7 +69,7 @@ class Polygon extends Shape {
|
||||
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
|
||||
Iterable<Vector2> scaled() {
|
||||
@ -82,7 +83,7 @@ class Polygon extends Shape {
|
||||
return _cachedScaledShape.value!;
|
||||
}
|
||||
|
||||
final _cachedRenderPath = ShapeCache<Path>();
|
||||
final _cachedRenderPath = ValueCache<Path>();
|
||||
|
||||
@override
|
||||
void render(Canvas canvas, Paint paint) {
|
||||
@ -114,7 +115,7 @@ class Polygon extends Shape {
|
||||
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
|
||||
/// are the "corners" of the hitbox rotated with [angle].
|
||||
|
||||
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
import '../../components.dart';
|
||||
import '../../game.dart';
|
||||
import '../../palette.dart';
|
||||
import '../components/cache/value_cache.dart';
|
||||
import '../extensions/vector2.dart';
|
||||
import 'shape_intersections.dart' as intersection_system;
|
||||
|
||||
@ -11,9 +12,9 @@ import 'shape_intersections.dart' as intersection_system;
|
||||
/// center.
|
||||
/// A point can be determined to be within of outside of a shape.
|
||||
abstract class Shape {
|
||||
final ShapeCache<Vector2> _halfSizeCache = ShapeCache();
|
||||
final ShapeCache<Vector2> _localCenterCache = ShapeCache();
|
||||
final ShapeCache<Vector2> _absoluteCenterCache = ShapeCache();
|
||||
final ValueCache<Vector2> _halfSizeCache = ValueCache();
|
||||
final ValueCache<Vector2> _localCenterCache = ValueCache();
|
||||
final ValueCache<Vector2> _absoluteCenterCache = ValueCache();
|
||||
|
||||
/// Should be the center of that [offsetPosition] and [relativeOffset]
|
||||
/// 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;
|
||||
|
||||
@override
|
||||
Vector2 get size => component.size;
|
||||
Vector2 get size => component.scaledSize;
|
||||
|
||||
@override
|
||||
double get parentAngle => component.angle;
|
||||
@ -159,32 +160,3 @@ typedef CollisionEndCallback = void Function(HitboxShape other);
|
||||
|
||||
void emptyCollisionCallback(Set<Vector2> _, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart' as material;
|
||||
|
||||
import 'anchor.dart';
|
||||
import 'components/cache/memory_cache.dart';
|
||||
import 'components/text_component.dart';
|
||||
import 'extensions/size.dart';
|
||||
import 'extensions/vector2.dart';
|
||||
import 'memory_cache.dart';
|
||||
|
||||
/// [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.
|
||||
|
||||
@ -256,6 +256,41 @@ void main() {
|
||||
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);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ void main() {
|
||||
bool isAlternating = false,
|
||||
bool hasAlternatingMoveEffect = false,
|
||||
bool hasAlternatingRotateEffect = false,
|
||||
bool hasAlternatingScaleEffect = false,
|
||||
bool hasAlternatingSizeEffect = false,
|
||||
}) {
|
||||
final move = MoveEffect(
|
||||
path: path,
|
||||
@ -39,10 +39,10 @@ void main() {
|
||||
duration: randomDuration(),
|
||||
isAlternating: hasAlternatingRotateEffect,
|
||||
)..skipEffectReset = true;
|
||||
final scale = ScaleEffect(
|
||||
final scale = SizeEffect(
|
||||
size: argumentSize,
|
||||
duration: randomDuration(),
|
||||
isAlternating: hasAlternatingScaleEffect,
|
||||
isAlternating: hasAlternatingSizeEffect,
|
||||
)..skipEffectReset = true;
|
||||
return CombinedEffect(
|
||||
effects: [move, scale, rotate],
|
||||
@ -169,13 +169,13 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'CombinedEffect can contain alternating ScaleEffect',
|
||||
'CombinedEffect can contain alternating SizeEffect',
|
||||
(WidgetTester tester) async {
|
||||
final PositionComponent positionComponent = component();
|
||||
effectTest(
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(hasAlternatingScaleEffect: true),
|
||||
effect(hasAlternatingSizeEffect: true),
|
||||
expectedPosition: path.last,
|
||||
expectedAngle: argumentAngle,
|
||||
expectedSize: positionComponent.size.clone(),
|
||||
|
||||
@ -25,9 +25,11 @@ void effectTest(
|
||||
double expectedAngle = 0.0,
|
||||
Vector2? expectedPosition,
|
||||
Vector2? expectedSize,
|
||||
Vector2? expectedScale,
|
||||
}) async {
|
||||
expectedPosition ??= Vector2.zero();
|
||||
expectedSize ??= Vector2.all(100.0);
|
||||
expectedScale ??= Vector2.all(1.0);
|
||||
final callback = Callback();
|
||||
effect.onComplete = callback.call;
|
||||
final game = BaseGame();
|
||||
@ -63,6 +65,11 @@ void effectTest(
|
||||
expectedSize,
|
||||
reason: 'Size is not correct',
|
||||
);
|
||||
expectVector2(
|
||||
component.scale,
|
||||
expectedScale,
|
||||
reason: 'Scale is not correct',
|
||||
);
|
||||
} else {
|
||||
// To account for float number operations making effects not finish
|
||||
const epsilon = 0.001;
|
||||
@ -87,6 +94,11 @@ void effectTest(
|
||||
expectedSize,
|
||||
reason: 'Size is not exactly correct',
|
||||
);
|
||||
expectVector2(
|
||||
component.scale,
|
||||
expectedScale,
|
||||
reason: 'Scale is not exactly correct',
|
||||
);
|
||||
}
|
||||
expect(effect.hasCompleted(), shouldComplete, reason: 'Effect shouldFinish');
|
||||
game.update(0.0);
|
||||
@ -103,11 +115,13 @@ class TestComponent extends PositionComponent {
|
||||
TestComponent({
|
||||
Vector2? position,
|
||||
Vector2? size,
|
||||
Vector2? scale,
|
||||
double angle = 0.0,
|
||||
Anchor anchor = Anchor.center,
|
||||
}) : super(
|
||||
position: position,
|
||||
size: size ?? Vector2.all(100.0),
|
||||
scale: scale,
|
||||
angle: angle,
|
||||
anchor: anchor,
|
||||
);
|
||||
|
||||
@ -9,12 +9,12 @@ 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());
|
||||
final argumentScale = randomVector2();
|
||||
TestComponent component() => TestComponent(scale: randomVector2());
|
||||
|
||||
ScaleEffect effect({bool isInfinite = false, bool isAlternating = false}) {
|
||||
return ScaleEffect(
|
||||
size: argumentSize,
|
||||
scale: argumentScale,
|
||||
duration: 1 + random.nextInt(100).toDouble(),
|
||||
isInfinite: isInfinite,
|
||||
isAlternating: isAlternating,
|
||||
@ -26,7 +26,7 @@ void main() {
|
||||
tester,
|
||||
component(),
|
||||
effect(),
|
||||
expectedSize: argumentSize,
|
||||
expectedScale: argumentScale,
|
||||
);
|
||||
});
|
||||
|
||||
@ -37,7 +37,7 @@ void main() {
|
||||
tester,
|
||||
component(),
|
||||
effect(),
|
||||
expectedSize: argumentSize,
|
||||
expectedScale: argumentScale,
|
||||
iterations: 1.5,
|
||||
);
|
||||
},
|
||||
@ -49,7 +49,7 @@ void main() {
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(isAlternating: true),
|
||||
expectedSize: positionComponent.size.clone(),
|
||||
expectedScale: positionComponent.scale.clone(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -61,7 +61,7 @@ void main() {
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(isInfinite: true, isAlternating: true),
|
||||
expectedSize: positionComponent.size.clone(),
|
||||
expectedScale: positionComponent.scale.clone(),
|
||||
shouldComplete: false,
|
||||
);
|
||||
},
|
||||
@ -73,7 +73,7 @@ void main() {
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(isAlternating: true),
|
||||
expectedSize: argumentSize,
|
||||
expectedScale: argumentScale,
|
||||
shouldComplete: false,
|
||||
iterations: 0.5,
|
||||
);
|
||||
@ -85,7 +85,7 @@ void main() {
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(isInfinite: true),
|
||||
expectedSize: argumentSize,
|
||||
expectedScale: argumentScale,
|
||||
iterations: 3.0,
|
||||
shouldComplete: false,
|
||||
);
|
||||
|
||||
@ -28,7 +28,7 @@ void main() {
|
||||
bool isAlternating = false,
|
||||
bool hasAlternatingMoveEffect = false,
|
||||
bool hasAlternatingRotateEffect = false,
|
||||
bool hasAlternatingScaleEffect = false,
|
||||
bool hasAlternatingSizeEffect = false,
|
||||
}) {
|
||||
final move = MoveEffect(
|
||||
path: path,
|
||||
@ -40,10 +40,10 @@ void main() {
|
||||
duration: randomDuration(),
|
||||
isAlternating: hasAlternatingRotateEffect,
|
||||
)..skipEffectReset = true;
|
||||
final scale = ScaleEffect(
|
||||
final scale = SizeEffect(
|
||||
size: argumentSize,
|
||||
duration: randomDuration(),
|
||||
isAlternating: hasAlternatingScaleEffect,
|
||||
isAlternating: hasAlternatingSizeEffect,
|
||||
)..skipEffectReset = true;
|
||||
return SequenceEffect(
|
||||
effects: [move, scale, rotate],
|
||||
@ -169,13 +169,13 @@ void main() {
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'SequenceEffect can contain alternating ScaleEffect',
|
||||
'SequenceEffect can contain alternating SizeEffect',
|
||||
(WidgetTester tester) async {
|
||||
final PositionComponent positionComponent = component();
|
||||
effectTest(
|
||||
tester,
|
||||
positionComponent,
|
||||
effect(hasAlternatingScaleEffect: true),
|
||||
effect(hasAlternatingSizeEffect: true),
|
||||
expectedPosition: path.last,
|
||||
expectedAngle: argumentAngle,
|
||||
expectedSize: positionComponent.size.clone(),
|
||||
|
||||
93
packages/flame/test/effects/size_effect_test.dart
Normal file
93
packages/flame/test/effects/size_effect_test.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -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
|
||||
'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
|
||||
],
|
||||
);
|
||||
@ -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
|
||||
'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
|
||||
],
|
||||
);
|
||||
@ -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
|
||||
'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
|
||||
],
|
||||
// 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
|
||||
'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
|
||||
],
|
||||
// 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
|
||||
'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
|
||||
],
|
||||
);
|
||||
@ -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
|
||||
'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
|
||||
],
|
||||
);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:flame/src/components/cache/memory_cache.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:flame/src/memory_cache.dart';
|
||||
|
||||
void main() {
|
||||
group('MemoryCache', () {
|
||||
test('basic cache addition', () {
|
||||
|
||||
Reference in New Issue
Block a user