mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Add HasTimeScale mixin (#2431)
This PR adds a new mixin on Component. When attached to a component, it allows scaling the delta time of that component as well as all its children by a non-negative factor. The idea is to allows slowing down or speeding up the gameplay by change the scaling factor. Note: This approach works only for framerate independent game logic. Code in update() that is not dependent on delta time will remain unaffected by time scale.
This commit is contained in:
@ -30,6 +30,7 @@ import 'package:doc_flame_examples/sequence_effect.dart';
|
|||||||
import 'package:doc_flame_examples/size_by_effect.dart';
|
import 'package:doc_flame_examples/size_by_effect.dart';
|
||||||
import 'package:doc_flame_examples/size_to_effect.dart';
|
import 'package:doc_flame_examples/size_to_effect.dart';
|
||||||
import 'package:doc_flame_examples/tap_events.dart';
|
import 'package:doc_flame_examples/tap_events.dart';
|
||||||
|
import 'package:doc_flame_examples/time_scale.dart';
|
||||||
import 'package:doc_flame_examples/value_route.dart';
|
import 'package:doc_flame_examples/value_route.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@ -71,6 +72,7 @@ void main() {
|
|||||||
'glow_effect': GlowEffectExample.new,
|
'glow_effect': GlowEffectExample.new,
|
||||||
'remove_effect': RemoveEffectGame.new,
|
'remove_effect': RemoveEffectGame.new,
|
||||||
'color_effect': ColorEffectExample.new,
|
'color_effect': ColorEffectExample.new,
|
||||||
|
'time_scale': TimeScaleGame.new,
|
||||||
};
|
};
|
||||||
final game = routes[page]?.call();
|
final game = routes[page]?.call();
|
||||||
if (game != null) {
|
if (game != null) {
|
||||||
|
|||||||
31
doc/flame/examples/lib/time_scale.dart
Normal file
31
doc/flame/examples/lib/time_scale.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:doc_flame_examples/ember.dart';
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/experimental.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
|
||||||
|
class TimeScaleGame extends FlameGame with HasTimeScale, HasTappableComponents {
|
||||||
|
final _timeScales = [0.5, 1.0, 2.0];
|
||||||
|
var _index = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await add(
|
||||||
|
EmberPlayer(
|
||||||
|
position: size / 2,
|
||||||
|
size: size / 4,
|
||||||
|
onTap: (p0) => timeScale = getNextTimeScale(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
double getNextTimeScale() {
|
||||||
|
++_index;
|
||||||
|
if (_index >= _timeScales.length) {
|
||||||
|
_index = 0;
|
||||||
|
}
|
||||||
|
return _timeScales[_index];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -146,6 +146,45 @@ class MyFlameGame extends FlameGame {
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Time Scale
|
||||||
|
|
||||||
|
In many games it is often desirable to create slow-motion or fast-forward effects based on some in
|
||||||
|
game events. A very common approach to achieve these results is to manipulate the in game time or
|
||||||
|
tick rate.
|
||||||
|
|
||||||
|
To make this manipulation easier, Flame provides a `HasTimeScale` mixin. This mixin can be attached
|
||||||
|
to any Flame `Component` and exposes a simple get/set API for `timeScale`. The default value of
|
||||||
|
`timeScale` is `1`, implying in-game time of the component is running at the same speed as real life
|
||||||
|
time. Setting it to `2` will make the component tick twice as fast and setting it to `0.5` will make
|
||||||
|
it tick at half the speed as compared to real life time.
|
||||||
|
|
||||||
|
Since `FlameGame` is a `Component` too, this mixin can be attached to the `FlameGame` as well. Doing
|
||||||
|
so will allow controlling time scale for all the component of the game from a single place.
|
||||||
|
|
||||||
|
```{flutter-app}
|
||||||
|
:sources: ../flame/examples
|
||||||
|
:page: time_scale
|
||||||
|
:show: widget code infobox
|
||||||
|
:width: 180
|
||||||
|
:height: 160
|
||||||
|
```
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
|
||||||
|
class MyFlameGame extends FlameGame with HasTimeScale {
|
||||||
|
void speedUp(){
|
||||||
|
timeScale = 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void slowDown(){
|
||||||
|
timeScale = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Extensions
|
## Extensions
|
||||||
|
|
||||||
Flame bundles a collection of utility extensions, these extensions are meant to help the developer
|
Flame bundles a collection of utility extensions, these extensions are meant to help the developer
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import 'package:examples/stories/components/game_in_game_example.dart';
|
|||||||
import 'package:examples/stories/components/look_at_example.dart';
|
import 'package:examples/stories/components/look_at_example.dart';
|
||||||
import 'package:examples/stories/components/look_at_smooth_example.dart';
|
import 'package:examples/stories/components/look_at_smooth_example.dart';
|
||||||
import 'package:examples/stories/components/priority_example.dart';
|
import 'package:examples/stories/components/priority_example.dart';
|
||||||
|
import 'package:examples/stories/components/time_scale_example.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
|
|
||||||
void addComponentsStories(Dashbook dashbook) {
|
void addComponentsStories(Dashbook dashbook) {
|
||||||
@ -67,5 +68,13 @@ void addComponentsStories(Dashbook dashbook) {
|
|||||||
codeLink:
|
codeLink:
|
||||||
baseLink('components/components_notifier_provider_example.dart'),
|
baseLink('components/components_notifier_provider_example.dart'),
|
||||||
info: ComponentsNotifierProviderExampleWidget.description,
|
info: ComponentsNotifierProviderExampleWidget.description,
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
'Time Scale',
|
||||||
|
(_) => const GameWidget.controlled(
|
||||||
|
gameFactory: TimeScaleExample.new,
|
||||||
|
),
|
||||||
|
codeLink: baseLink('components/time_scale_example.dart'),
|
||||||
|
info: TimeScaleExample.description,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
123
examples/lib/stories/components/time_scale_example.dart
Normal file
123
examples/lib/stories/components/time_scale_example.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flame/collisions.dart';
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame/image_composition.dart';
|
||||||
|
import 'package:flame/palette.dart';
|
||||||
|
import 'package:flame/sprite.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class TimeScaleExample extends FlameGame
|
||||||
|
with HasTimeScale, HasCollisionDetection {
|
||||||
|
static const description =
|
||||||
|
'This example shows how time scale can be used to control game speed.';
|
||||||
|
|
||||||
|
final gameSpeedText = TextComponent(
|
||||||
|
text: 'Time Scale: 1',
|
||||||
|
textRenderer: TextPaint(
|
||||||
|
style: TextStyle(
|
||||||
|
color: BasicPalette.white.color,
|
||||||
|
fontSize: 20.0,
|
||||||
|
shadows: const [
|
||||||
|
Shadow(offset: Offset(1, 1), blurRadius: 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color backgroundColor() => const Color.fromARGB(255, 88, 114, 97);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
camera.viewport = FixedResolutionViewport(Vector2(640, 360));
|
||||||
|
final spriteSheet = SpriteSheet(
|
||||||
|
image: await images.load('animations/chopper.png'),
|
||||||
|
srcSize: Vector2.all(48),
|
||||||
|
);
|
||||||
|
gameSpeedText.position = Vector2(size.x * 0.5, size.y * 0.8);
|
||||||
|
|
||||||
|
await addAll([
|
||||||
|
_Chopper(
|
||||||
|
position: Vector2(size.x * 0.3, size.y * 0.45),
|
||||||
|
size: Vector2.all(64),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
angle: -pi / 2,
|
||||||
|
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
|
||||||
|
),
|
||||||
|
_Chopper(
|
||||||
|
position: Vector2(size.x * 0.6, size.y * 0.55),
|
||||||
|
size: Vector2.all(64),
|
||||||
|
anchor: Anchor.center,
|
||||||
|
angle: pi / 2,
|
||||||
|
animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05),
|
||||||
|
),
|
||||||
|
gameSpeedText,
|
||||||
|
]);
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
gameSpeedText.text = 'Time Scale : $timeScale';
|
||||||
|
super.update(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Chopper extends SpriteAnimationComponent
|
||||||
|
with HasGameRef<TimeScaleExample>, CollisionCallbacks {
|
||||||
|
_Chopper({
|
||||||
|
super.animation,
|
||||||
|
super.position,
|
||||||
|
super.size,
|
||||||
|
super.angle,
|
||||||
|
super.anchor,
|
||||||
|
}) : _moveDirection = Vector2(0, 1)..rotate(angle ?? 0),
|
||||||
|
_initialPosition = position?.clone() ?? Vector2.zero();
|
||||||
|
|
||||||
|
final Vector2 _moveDirection;
|
||||||
|
final _speed = 80.0;
|
||||||
|
final Vector2 _initialPosition;
|
||||||
|
late final _timer = TimerComponent(
|
||||||
|
period: 2,
|
||||||
|
onTick: _reset,
|
||||||
|
autoStart: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await add(CircleHitbox());
|
||||||
|
await add(_timer);
|
||||||
|
return super.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateTree(double dt) {
|
||||||
|
position.setFrom(position + _moveDirection * _speed * dt);
|
||||||
|
super.updateTree(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onCollisionStart(Set<Vector2> _, PositionComponent other) {
|
||||||
|
if (other is _Chopper) {
|
||||||
|
gameRef.timeScale = 0.25;
|
||||||
|
}
|
||||||
|
super.onCollisionStart(_, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onCollisionEnd(PositionComponent other) {
|
||||||
|
if (other is _Chopper) {
|
||||||
|
gameRef.timeScale = 1.0;
|
||||||
|
_timer.timer.start();
|
||||||
|
}
|
||||||
|
super.onCollisionEnd(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reset() {
|
||||||
|
position.setFrom(_initialPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ export 'src/components/mixins/has_ancestor.dart';
|
|||||||
export 'src/components/mixins/has_decorator.dart' show HasDecorator;
|
export 'src/components/mixins/has_decorator.dart' show HasDecorator;
|
||||||
export 'src/components/mixins/has_game_ref.dart' show HasGameRef;
|
export 'src/components/mixins/has_game_ref.dart' show HasGameRef;
|
||||||
export 'src/components/mixins/has_paint.dart';
|
export 'src/components/mixins/has_paint.dart';
|
||||||
|
export 'src/components/mixins/has_time_scale.dart';
|
||||||
export 'src/components/mixins/hoverable.dart';
|
export 'src/components/mixins/hoverable.dart';
|
||||||
export 'src/components/mixins/keyboard_handler.dart';
|
export 'src/components/mixins/keyboard_handler.dart';
|
||||||
export 'src/components/mixins/notifier.dart';
|
export 'src/components/mixins/notifier.dart';
|
||||||
|
|||||||
37
packages/flame/lib/src/components/mixins/has_time_scale.dart
Normal file
37
packages/flame/lib/src/components/mixins/has_time_scale.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import 'package:flame/src/components/core/component.dart';
|
||||||
|
|
||||||
|
/// This mixin allows components to control their speed as compared to the
|
||||||
|
/// normal speed. Only framerate independent logic will benefit [timeScale]
|
||||||
|
/// changes.
|
||||||
|
///
|
||||||
|
/// Note: Modified [timeScale] will be applied to all children as well.
|
||||||
|
mixin HasTimeScale on Component {
|
||||||
|
/// The ratio of components tick speed and normal tick speed.
|
||||||
|
/// It defaults to 1.0, which means the component moves normally.
|
||||||
|
/// A value of 0.5 means the component moves half the normal speed
|
||||||
|
/// and a value of 2.0 means the component moves twice as fast.
|
||||||
|
double _timeScale = 1.0;
|
||||||
|
|
||||||
|
/// Returns the current time scale.
|
||||||
|
double get timeScale => _timeScale;
|
||||||
|
|
||||||
|
/// Sets the time scale to given value if it is non-negative.
|
||||||
|
/// Note: Too high values will result in inconsistent gameplay
|
||||||
|
/// and tunneling in physics.
|
||||||
|
set timeScale(double value) {
|
||||||
|
if (value.isNegative) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_timeScale = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt * (parent == null ? _timeScale : 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateTree(double dt) {
|
||||||
|
super.updateTree(dt * (parent != null ? _timeScale : 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('HasTimeScale', () {
|
||||||
|
testWithGame<_GameWithTimeScale>(
|
||||||
|
'delta time scales correctly',
|
||||||
|
_GameWithTimeScale.new,
|
||||||
|
(game) async {
|
||||||
|
final component = _MovingComponent();
|
||||||
|
await game.add(component);
|
||||||
|
await game.ready();
|
||||||
|
const stepTime = 10.0;
|
||||||
|
var distance = 0.0;
|
||||||
|
final offset = stepTime * component.speed;
|
||||||
|
|
||||||
|
game.timeScale = 0.5;
|
||||||
|
distance = component.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(component.x, distance + game.timeScale * offset);
|
||||||
|
|
||||||
|
game.timeScale = 1.0;
|
||||||
|
distance = component.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(component.x, distance + game.timeScale * offset);
|
||||||
|
|
||||||
|
game.timeScale = 1.5;
|
||||||
|
distance = component.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(component.x, distance + game.timeScale * offset);
|
||||||
|
|
||||||
|
game.timeScale = 2.0;
|
||||||
|
distance = component.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(component.x, distance + game.timeScale * offset);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWithGame(
|
||||||
|
'cascading time scale',
|
||||||
|
_GameWithTimeScale.new,
|
||||||
|
(game) async {
|
||||||
|
final component1 = _ComponentWithTimeScale();
|
||||||
|
final component2 = _MovingComponent();
|
||||||
|
await component1.add(component2);
|
||||||
|
await game.add(component1);
|
||||||
|
await game.ready();
|
||||||
|
const stepTime = 10.0;
|
||||||
|
var distance = 0.0;
|
||||||
|
final offset = stepTime * component2.speed;
|
||||||
|
|
||||||
|
game.timeScale = 0.5;
|
||||||
|
component1.timeScale = 0.5;
|
||||||
|
distance = component2.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(
|
||||||
|
component2.x,
|
||||||
|
distance + game.timeScale * component1.timeScale * offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
game.timeScale = 1.0;
|
||||||
|
distance = component2.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(
|
||||||
|
component2.x,
|
||||||
|
distance + game.timeScale * component1.timeScale * offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
component1.timeScale = 1.5;
|
||||||
|
distance = component2.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(
|
||||||
|
component2.x,
|
||||||
|
distance + game.timeScale * component1.timeScale * offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
game.timeScale = 2.0;
|
||||||
|
distance = component2.x;
|
||||||
|
game.update(stepTime);
|
||||||
|
expect(
|
||||||
|
component2.x,
|
||||||
|
distance + game.timeScale * component1.timeScale * offset,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GameWithTimeScale extends FlameGame with HasTimeScale {}
|
||||||
|
|
||||||
|
class _ComponentWithTimeScale extends Component with HasTimeScale {}
|
||||||
|
|
||||||
|
class _MovingComponent extends PositionComponent {
|
||||||
|
final speed = 1.0;
|
||||||
|
@override
|
||||||
|
void update(double dt) => position.setValues(position.x + speed * dt, 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user