Skip to content

Commit

Permalink
feat: Notifier for changing current sprite/animation in group compone…
Browse files Browse the repository at this point in the history
…nts (#2956)

With these new notifiers you can get notified when the current animation
or sprite changes in the group components.
The value notifier is not initialized unless the user tries to use it,
so it doesn't carry any extra weight on the components.
  • Loading branch information
spydon authored Jan 4, 2024
1 parent 7969321 commit 75cf239
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/sprite_animation_ticker.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';

export '../sprite_animation.dart';

Expand All @@ -13,6 +13,12 @@ class SpriteAnimationGroupComponent<T> extends PositionComponent
/// Key with the current playing animation
T? _current;

ValueNotifier<T?>? _currentAnimationNotifier;

/// A [ValueNotifier] that notifies when the current animation changes.
ValueNotifier<T?> get currentAnimationNotifier =>
_currentAnimationNotifier ??= ValueNotifier<T?>(_current);

/// Map with the mapping each state to the flag removeOnFinish
final Map<T, bool> removeOnFinish;

Expand Down Expand Up @@ -136,8 +142,11 @@ class SpriteAnimationGroupComponent<T> extends PositionComponent
_current = value;
_resizeToSprite();

if (changed && autoResetTicker) {
animationTicker?.reset();
if (changed) {
if (autoResetTicker) {
animationTicker?.reset();
}
_currentAnimationNotifier?.value = value;
}
}

Expand Down
18 changes: 14 additions & 4 deletions packages/flame/lib/src/components/sprite_group_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';

export '../sprite_animation.dart';

Expand All @@ -11,17 +11,23 @@ export '../sprite_animation.dart';
class SpriteGroupComponent<T> extends PositionComponent
with HasPaint
implements SizeProvider {
/// Key with the current playing animation
/// Key for the current sprite.
T? _current;

/// Map with the available states for this sprite group
ValueNotifier<T?>? _currentSpriteNotifier;

/// A [ValueNotifier] that notifies when the current sprite changes.
ValueNotifier<T?> get currentSpriteNotifier =>
_currentSpriteNotifier ??= ValueNotifier<T?>(_current);

/// Map with the available states for this sprite group.
Map<T, Sprite>? _sprites;

/// When set to true, the component is auto-resized to match the
/// size of current sprite.
bool _autoResize;

/// Creates a component with an empty animation which can be set later
/// Creates a component with an empty animation which can be set later.
SpriteGroupComponent({
Map<T, Sprite>? sprites,
T? current,
Expand Down Expand Up @@ -62,8 +68,12 @@ class SpriteGroupComponent<T> extends PositionComponent
///
/// Will update [size] if [autoResize] is true.
set current(T? value) {
final changed = _current != value;
_current = value;
_resizeToSprite();
if (changed) {
_currentSpriteNotifier?.value = value;
}
}

/// Returns current value of auto resize flag.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,122 +199,8 @@ Future<void> main() async {
});
});

group('SpriteAnimationGroupComponent.autoResize', () {
test('mutual exclusive with size while construction', () {
expect(
() => SpriteAnimationGroupComponent<_AnimationState>(
autoResize: true,
size: Vector2.all(2),
),
throwsAssertionError,
);

expect(
() => SpriteAnimationGroupComponent<_AnimationState>(autoResize: false),
throwsAssertionError,
);
});

test('default value set correctly when not provided explicitly', () {
final component1 = SpriteAnimationGroupComponent<_AnimationState>();
final component2 = SpriteAnimationGroupComponent<_AnimationState>(
size: Vector2.all(2),
);

expect(component1.autoResize, true);
expect(component2.autoResize, false);
});

test('resizes on current state change', () {
final sprite1 = Sprite(image, srcSize: Vector2.all(76));
final sprite2 = Sprite(image, srcSize: Vector2.all(15));
final animation1 = SpriteAnimation.spriteList(
List.filled(5, sprite1),
stepTime: 0.1,
loop: false,
);
final animation2 = SpriteAnimation.spriteList(
List.filled(5, sprite2),
stepTime: 0.1,
loop: false,
);

final component = SpriteAnimationGroupComponent<_AnimationState>(
animations: {
_AnimationState.idle: animation1,
_AnimationState.running: animation2,
},
current: _AnimationState.idle,
);
expect(component.size, sprite1.srcSize);

component.current = _AnimationState.running;
expect(component.size, sprite2.srcSize);
});

test('resizes only when true', () {
final sprite1 = Sprite(image, srcSize: Vector2.all(76));
final sprite2 = Sprite(image, srcSize: Vector2.all(15));
final animation1 = SpriteAnimation.spriteList(
List.filled(5, sprite1),
stepTime: 0.1,
loop: false,
);
final animation2 = SpriteAnimation.spriteList(
List.filled(5, sprite2),
stepTime: 0.1,
loop: false,
);

final component = SpriteAnimationGroupComponent<_AnimationState>(
animations: {
_AnimationState.idle: animation1,
_AnimationState.running: animation2,
},
current: _AnimationState.idle,
)..autoResize = false;

component.current = _AnimationState.running;
expect(component.size, sprite1.srcSize);

component.autoResize = true;
expect(component.size, sprite2.srcSize);
});

test('stop autoResizing on external size modifications', () {
final testSize = Vector2(83, 100);
final sprite1 = Sprite(image, srcSize: Vector2.all(76));
final sprite2 = Sprite(image, srcSize: Vector2.all(15));
final animation1 = SpriteAnimation.spriteList(
List.filled(5, sprite1),
stepTime: 0.1,
loop: false,
);
final animation2 = SpriteAnimation.spriteList(
List.filled(5, sprite2),
stepTime: 0.1,
loop: false,
);
final animationsMap = {
_AnimationState.idle: animation1,
_AnimationState.running: animation2,
};
final component = SpriteAnimationGroupComponent<_AnimationState>();

// NOTE: Sequence of modifications is important here. Changing the size
// after changing the animations map will disable auto-resizing. So even
// if the current state is changed later, the component should still
// maintain testSize.
component
..animations = animationsMap
..size = testSize
..current = _AnimationState.running;

expectDouble(component.size.x, testSize.x);
expectDouble(component.size.y, testSize.y);
});

test('modify size only if changed while auto-resizing', () {
group('SpriteAnimationGroupComponent.currentAnimationNotifier', () {
test('notifies when the current animation changes', () {
final sprite1 = Sprite(image, srcSize: Vector2.all(76));
final sprite2 = Sprite(image, srcSize: Vector2.all(15));
final animation1 = SpriteAnimation.spriteList(
Expand All @@ -333,84 +219,29 @@ Future<void> main() async {
final component = SpriteAnimationGroupComponent<_AnimationState>(
animations: animationsMap,
);

var sizeChangeCounter = 0;
component.size.addListener(() => ++sizeChangeCounter);
var animationChangeCounter = 0;
component.currentAnimationNotifier.addListener(
() => animationChangeCounter++,
);

component.current = _AnimationState.running;
expect(sizeChangeCounter, equals(1));
expect(animationChangeCounter, equals(1));

component.current = _AnimationState.idle;
expect(sizeChangeCounter, equals(2));
expect(animationChangeCounter, equals(2));

component.update(1);
expect(sizeChangeCounter, equals(2));
expect(animationChangeCounter, equals(2));

component.current = _AnimationState.running;
expect(sizeChangeCounter, equals(3));
expect(animationChangeCounter, equals(3));

component.update(1);
expect(sizeChangeCounter, equals(4));
});
});

group('SpriteAnimationGroupComponent.autoResetTicker', () {
final sprite1 = Sprite(image, srcSize: Vector2.all(76));
final sprite2 = Sprite(image, srcSize: Vector2.all(15));
final animation1 = SpriteAnimation.spriteList(
List.filled(5, sprite1),
stepTime: 0.1,
loop: false,
);
final animation2 = SpriteAnimation.spriteList(
List.filled(5, sprite2),
stepTime: 0.1,
loop: false,
);

test('does reset ticker by default', () {
final component = SpriteAnimationGroupComponent<_AnimationState>(
animations: {
_AnimationState.idle: animation1,
_AnimationState.running: animation2,
},
current: _AnimationState.idle,
);
component.update(0.9);
expect(component.animationTicker!.currentIndex, 4);
expect(animationChangeCounter, equals(3));

component.current = _AnimationState.running;
component.update(0.1);
expect(component.animationTicker!.currentIndex, 1);

component.current = _AnimationState.idle;
expect(component.animationTicker!.currentIndex, 0);

component.current = _AnimationState.running;
expect(component.animationTicker!.currentIndex, 0);
});

test('resets the ticker when enabled', () {
final component = SpriteAnimationGroupComponent<_AnimationState>(
animations: {
_AnimationState.idle: animation1,
_AnimationState.running: animation2,
},
autoResetTicker: false,
current: _AnimationState.idle,
);
component.update(0.9);
expect(component.animationTicker!.currentIndex, 4);

component.current = _AnimationState.running;
component.update(0.1);
expect(component.animationTicker!.currentIndex, 1);

component.current = _AnimationState.idle;
expect(component.animationTicker!.currentIndex, 4);

component.current = _AnimationState.running;
expect(component.animationTicker!.currentIndex, 1);
component.update(1);
expect(animationChangeCounter, equals(3));
});
});
}
36 changes: 36 additions & 0 deletions packages/flame/test/components/sprite_group_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,40 @@ Future<void> main() async {
expect(sizeChangeCounter, equals(2));
});
});

group('SpriteGroupComponent.currentSpriteNotifier', () {
test('notifies when the current sprite changes', () {
final spritesMap = {
_SpriteState.idle: Sprite(image, srcSize: Vector2.all(15)),
_SpriteState.running: Sprite(image, srcSize: Vector2.all(15)),
_SpriteState.flying: Sprite(image, srcSize: Vector2(15, 12)),
};
final component = SpriteGroupComponent<_SpriteState>(
sprites: spritesMap,
);
var spriteChangeCounter = 0;
component.currentSpriteNotifier.addListener(
() => spriteChangeCounter++,
);

component.current = _SpriteState.running;
expect(spriteChangeCounter, equals(1));

component.current = _SpriteState.idle;
expect(spriteChangeCounter, equals(2));

component.update(1);
expect(spriteChangeCounter, equals(2));

component.current = _SpriteState.running;
expect(spriteChangeCounter, equals(3));

component.update(1);
expect(spriteChangeCounter, equals(3));

component.current = _SpriteState.running;
component.update(1);
expect(spriteChangeCounter, equals(3));
});
});
}

0 comments on commit 75cf239

Please sign in to comment.