mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 01:18:38 +08:00
Fixing Flame pause and resume engine features (#1066)
* Fixing Flame pause and resume engine features * Update packages/flame/lib/src/game/mixins/game.dart * Apply suggestions from code review Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com> * Update doc/game.md * Update doc/game.md Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
10
doc/game.md
10
doc/game.md
@ -150,6 +150,16 @@ built upon two methods:
|
|||||||
|
|
||||||
The `GameLoop` is used by all of Flame's `Game` implementations.
|
The `GameLoop` is used by all of Flame's `Game` implementations.
|
||||||
|
|
||||||
|
# Pause/Resuming game execution
|
||||||
|
|
||||||
|
A Flame `Game` can be paused and resumed in two ways:
|
||||||
|
|
||||||
|
- With the use of the `pauseEngine` and `resumeEngine` methods.
|
||||||
|
- By changing the `paused` attribute.
|
||||||
|
|
||||||
|
When pausing a Flame `Game`, the `GameLoop` is effectively paused, meaning that no updates or new
|
||||||
|
renders will happen until it is resumed.
|
||||||
|
|
||||||
# Flutter Widgets and Game instances
|
# Flutter Widgets and Game instances
|
||||||
|
|
||||||
Since a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter
|
Since a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import 'stories/input/input.dart';
|
|||||||
import 'stories/parallax/parallax.dart';
|
import 'stories/parallax/parallax.dart';
|
||||||
import 'stories/rendering/rendering.dart';
|
import 'stories/rendering/rendering.dart';
|
||||||
import 'stories/sprites/sprites.dart';
|
import 'stories/sprites/sprites.dart';
|
||||||
|
import 'stories/system/system.dart';
|
||||||
import 'stories/tile_maps/tile_maps.dart';
|
import 'stories/tile_maps/tile_maps.dart';
|
||||||
import 'stories/utils/utils.dart';
|
import 'stories/utils/utils.dart';
|
||||||
import 'stories/widgets/widgets.dart';
|
import 'stories/widgets/widgets.dart';
|
||||||
@ -32,6 +33,7 @@ void main() async {
|
|||||||
addCameraAndViewportStories(dashbook);
|
addCameraAndViewportStories(dashbook);
|
||||||
addParallaxStories(dashbook);
|
addParallaxStories(dashbook);
|
||||||
addWidgetsStories(dashbook);
|
addWidgetsStories(dashbook);
|
||||||
|
addSystemStories(dashbook);
|
||||||
|
|
||||||
runApp(dashbook);
|
runApp(dashbook);
|
||||||
}
|
}
|
||||||
|
|||||||
50
examples/lib/stories/system/pause_resume_game.dart
Normal file
50
examples/lib/stories/system/pause_resume_game.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame/input.dart';
|
||||||
|
|
||||||
|
class PauseResumeGame extends FlameGame with TapDetector, DoubleTapDetector {
|
||||||
|
static const info = '''
|
||||||
|
Demonstrate how to use the pause and resume engine methods and paused attribute.
|
||||||
|
|
||||||
|
Tap on the screen to toggle the execution of the engine using the `resumeEngine` and
|
||||||
|
`pauseEngine`
|
||||||
|
|
||||||
|
Double Tap on the screen to toggle the execution of the engine using the `paused` attribute
|
||||||
|
''';
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
final animation = await loadSpriteAnimation(
|
||||||
|
'animations/chopper.png',
|
||||||
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: 4,
|
||||||
|
textureSize: Vector2.all(48),
|
||||||
|
stepTime: 0.15,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
add(
|
||||||
|
SpriteAnimationComponent(
|
||||||
|
animation: animation,
|
||||||
|
)
|
||||||
|
..position = size / 2
|
||||||
|
..anchor = Anchor.center
|
||||||
|
..size = Vector2.all(100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTap() {
|
||||||
|
if (paused) {
|
||||||
|
resumeEngine();
|
||||||
|
} else {
|
||||||
|
pauseEngine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDoubleTap() {
|
||||||
|
paused = !paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/lib/stories/system/system.dart
Normal file
15
examples/lib/stories/system/system.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
|
||||||
|
import '../../commons/commons.dart';
|
||||||
|
import 'pause_resume_game.dart';
|
||||||
|
|
||||||
|
void addSystemStories(Dashbook dashbook) {
|
||||||
|
dashbook.storiesOf('System')
|
||||||
|
..add(
|
||||||
|
'Pause/resume engine',
|
||||||
|
(_) => GameWidget(game: PauseResumeGame()),
|
||||||
|
codeLink: baseLink('system/pause_resume_game.dart'),
|
||||||
|
info: PauseResumeGame.info,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,24 +1,10 @@
|
|||||||
import 'package:dashbook/dashbook.dart';
|
import 'package:dashbook/dashbook.dart';
|
||||||
|
import 'package:flame/components.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';
|
||||||
|
|
||||||
Widget overlayBuilder(DashbookContext ctx) {
|
Widget _pauseMenuBuilder(BuildContext buildContext, ExampleGame game) {
|
||||||
return const OverlayExampleWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
class OverlayExampleWidget extends StatefulWidget {
|
|
||||||
const OverlayExampleWidget({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
_OverlayExampleWidgetState createState() => _OverlayExampleWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OverlayExampleWidgetState extends State<OverlayExampleWidget> {
|
|
||||||
ExampleGame? _myGame;
|
|
||||||
|
|
||||||
Widget pauseMenuBuilder(BuildContext buildContext, ExampleGame game) {
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
@ -31,43 +17,37 @@ class _OverlayExampleWidgetState extends State<OverlayExampleWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Widget overlayBuilder(DashbookContext ctx) {
|
||||||
Widget build(BuildContext context) {
|
return GameWidget<ExampleGame>(
|
||||||
final myGame = _myGame;
|
game: ExampleGame()..paused = true,
|
||||||
return Scaffold(
|
overlayBuilderMap: const {
|
||||||
appBar: AppBar(
|
'PauseMenu': _pauseMenuBuilder,
|
||||||
title: const Text('Testing addingOverlay'),
|
|
||||||
),
|
|
||||||
body: myGame == null
|
|
||||||
? const Text('Wait')
|
|
||||||
: GameWidget<ExampleGame>(
|
|
||||||
game: myGame,
|
|
||||||
overlayBuilderMap: {
|
|
||||||
'PauseMenu': pauseMenuBuilder,
|
|
||||||
},
|
},
|
||||||
initialActiveOverlays: const ['PauseMenu'],
|
initialActiveOverlays: const ['PauseMenu'],
|
||||||
),
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: newGame,
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void newGame() {
|
|
||||||
setState(() {
|
|
||||||
_myGame = ExampleGame();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExampleGame extends FlameGame with TapDetector {
|
class ExampleGame extends FlameGame with TapDetector {
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
Future<void> onLoad() async {
|
||||||
super.render(canvas);
|
await super.onLoad();
|
||||||
canvas.drawRect(
|
final animation = await loadSpriteAnimation(
|
||||||
const Rect.fromLTWH(100, 100, 100, 100),
|
'animations/chopper.png',
|
||||||
Paint()..color = BasicPalette.white.color,
|
SpriteAnimationData.sequenced(
|
||||||
|
amount: 4,
|
||||||
|
textureSize: Vector2.all(48),
|
||||||
|
stepTime: 0.15,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
add(
|
||||||
|
SpriteAnimationComponent(
|
||||||
|
animation: animation,
|
||||||
|
)
|
||||||
|
..position.y = size.y / 2
|
||||||
|
..position.x = 100
|
||||||
|
..anchor = Anchor.center
|
||||||
|
..size = Vector2.all(100),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,8 +55,10 @@ class ExampleGame extends FlameGame with TapDetector {
|
|||||||
void onTap() {
|
void onTap() {
|
||||||
if (overlays.isActive('PauseMenu')) {
|
if (overlays.isActive('PauseMenu')) {
|
||||||
overlays.remove('PauseMenu');
|
overlays.remove('PauseMenu');
|
||||||
|
resumeEngine();
|
||||||
} else {
|
} else {
|
||||||
overlays.add('PauseMenu');
|
overlays.add('PauseMenu');
|
||||||
|
pauseEngine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
- Added `StandardEffectController` class
|
- Added `StandardEffectController` class
|
||||||
- Refactored `Effect` class to use `EffectController`, added `Transform2DEffect` class
|
- Refactored `Effect` class to use `EffectController`, added `Transform2DEffect` class
|
||||||
- Clarified `TimerComponent` example
|
- Clarified `TimerComponent` example
|
||||||
|
- Fixed pause and resume engines when `GameWidget` had rebuilds
|
||||||
|
- Removed `runOnCreation` attribute in favor of the `paused` attribute on `FlameGame`
|
||||||
- Add `CustomPainterComponent`
|
- Add `CustomPainterComponent`
|
||||||
- Alternative implementation of `RotateEffect`, based on `Transform2DEffect`
|
- Alternative implementation of `RotateEffect`, based on `Transform2DEffect`
|
||||||
- Fix `onGameResize` margin bug in `HudMarginComponent`
|
- Fix `onGameResize` margin bug in `HudMarginComponent`
|
||||||
|
|||||||
@ -39,5 +39,10 @@ class GameLoop {
|
|||||||
|
|
||||||
void resume() {
|
void resume() {
|
||||||
_ticker.muted = false;
|
_ticker.muted = false;
|
||||||
|
// If the game has started paused, we need to start the ticker
|
||||||
|
// as it would not have been started yet
|
||||||
|
if (!_ticker.isActive) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver {
|
|||||||
game.pauseEngineFn = gameLoop.pause;
|
game.pauseEngineFn = gameLoop.pause;
|
||||||
game.resumeEngineFn = gameLoop.resume;
|
game.resumeEngineFn = gameLoop.resume;
|
||||||
|
|
||||||
if (game.runOnCreation) {
|
if (!game.paused) {
|
||||||
gameLoop.start();
|
gameLoop.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -197,14 +197,35 @@ mixin Game on Loadable implements Projector {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flag to tell the game loop if it should start running upon creation.
|
bool _paused = false;
|
||||||
bool runOnCreation = true;
|
|
||||||
|
/// Returns is the engine if currently paused or running
|
||||||
|
bool get paused => _paused;
|
||||||
|
|
||||||
|
/// Pauses or resume the engine
|
||||||
|
set paused(bool value) {
|
||||||
|
if (pauseEngineFn != null && resumeEngineFn != null) {
|
||||||
|
if (value) {
|
||||||
|
pauseEngine();
|
||||||
|
} else {
|
||||||
|
resumeEngine();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_paused = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pauses the engine game loop execution.
|
/// Pauses the engine game loop execution.
|
||||||
void pauseEngine() => pauseEngineFn?.call();
|
void pauseEngine() {
|
||||||
|
_paused = true;
|
||||||
|
pauseEngineFn?.call();
|
||||||
|
}
|
||||||
|
|
||||||
/// Resumes the engine game loop execution.
|
/// Resumes the engine game loop execution.
|
||||||
void resumeEngine() => resumeEngineFn?.call();
|
void resumeEngine() {
|
||||||
|
_paused = false;
|
||||||
|
resumeEngineFn?.call();
|
||||||
|
}
|
||||||
|
|
||||||
VoidCallback? pauseEngineFn;
|
VoidCallback? pauseEngineFn;
|
||||||
VoidCallback? resumeEngineFn;
|
VoidCallback? resumeEngineFn;
|
||||||
|
|||||||
163
packages/flame/test/game/game_widget/game_widget_pause_test.dart
Normal file
163
packages/flame/test/game/game_widget/game_widget_pause_test.dart
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flame_test/flame_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
class _Wrapper extends StatefulWidget {
|
||||||
|
const _Wrapper({
|
||||||
|
required this.child,
|
||||||
|
this.small = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final bool small;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_Wrapper> createState() => _WrapperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WrapperState extends State<_Wrapper> {
|
||||||
|
late bool _small;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_small = widget.small;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: _small ? 50 : 100,
|
||||||
|
height: _small ? 50 : 100,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Toggle'),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _small = !_small);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyGame extends FlameGame {
|
||||||
|
int callCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
|
||||||
|
callCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
flameWidgetTest<MyGame>(
|
||||||
|
'can pause the engine',
|
||||||
|
createGame: () => MyGame(),
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(_Wrapper(child: gameWidget));
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.pauseEngine();
|
||||||
|
|
||||||
|
// shouldn't run another frame on the game
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(game.callCount, equals(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameWidgetTest<MyGame>(
|
||||||
|
'can resume the engine',
|
||||||
|
createGame: () => MyGame(),
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(_Wrapper(child: gameWidget));
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.pauseEngine();
|
||||||
|
|
||||||
|
// shouldn't run another frame on the game
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.resumeEngine();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(game.callCount, equals(3));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameWidgetTest<MyGame>(
|
||||||
|
"when paused, don't auto start after a rebuild",
|
||||||
|
createGame: () => MyGame(),
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(_Wrapper(child: gameWidget));
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.pauseEngine();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Toggle'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(game.callCount, equals(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameWidgetTest<MyGame>(
|
||||||
|
'can started paused',
|
||||||
|
createGame: () => MyGame()..paused = true,
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(_Wrapper(child: gameWidget));
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(game.callCount, equals(0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
flameWidgetTest<MyGame>(
|
||||||
|
'can started paused and resumed later',
|
||||||
|
createGame: () => MyGame()..paused = true,
|
||||||
|
pumpWidget: (gameWidget, tester) async {
|
||||||
|
await tester.pumpWidget(_Wrapper(child: gameWidget));
|
||||||
|
},
|
||||||
|
verify: (game, tester) async {
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
game.resumeEngine();
|
||||||
|
|
||||||
|
// Run two frames
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(game.callCount, equals(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user