mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +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_to_effect.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:flame/game.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -71,6 +72,7 @@ void main() {
|
||||
'glow_effect': GlowEffectExample.new,
|
||||
'remove_effect': RemoveEffectGame.new,
|
||||
'color_effect': ColorEffectExample.new,
|
||||
'time_scale': TimeScaleGame.new,
|
||||
};
|
||||
final game = routes[page]?.call();
|
||||
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
|
||||
|
||||
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_smooth_example.dart';
|
||||
import 'package:examples/stories/components/priority_example.dart';
|
||||
import 'package:examples/stories/components/time_scale_example.dart';
|
||||
import 'package:flame/game.dart';
|
||||
|
||||
void addComponentsStories(Dashbook dashbook) {
|
||||
@ -67,5 +68,13 @@ void addComponentsStories(Dashbook dashbook) {
|
||||
codeLink:
|
||||
baseLink('components/components_notifier_provider_example.dart'),
|
||||
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_game_ref.dart' show HasGameRef;
|
||||
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/keyboard_handler.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