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:
Erick
2021-11-02 20:08:53 -03:00
committed by GitHub
parent 45b21c7c5f
commit b8ea25eceb
10 changed files with 315 additions and 65 deletions

View File

@ -150,6 +150,16 @@ built upon two methods:
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
Since a Flame game can be wrapped in a widget, it is quite easy to use it alongside other Flutter

View File

@ -10,6 +10,7 @@ import 'stories/input/input.dart';
import 'stories/parallax/parallax.dart';
import 'stories/rendering/rendering.dart';
import 'stories/sprites/sprites.dart';
import 'stories/system/system.dart';
import 'stories/tile_maps/tile_maps.dart';
import 'stories/utils/utils.dart';
import 'stories/widgets/widgets.dart';
@ -32,6 +33,7 @@ void main() async {
addCameraAndViewportStories(dashbook);
addParallaxStories(dashbook);
addWidgetsStories(dashbook);
addSystemStories(dashbook);
runApp(dashbook);
}

View 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;
}
}

View 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,
);
}

View File

@ -1,24 +1,10 @@
import 'package:dashbook/dashbook.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
Widget overlayBuilder(DashbookContext ctx) {
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) {
Widget _pauseMenuBuilder(BuildContext buildContext, ExampleGame game) {
return Center(
child: Container(
width: 100,
@ -31,43 +17,37 @@ class _OverlayExampleWidgetState extends State<OverlayExampleWidget> {
);
}
@override
Widget build(BuildContext context) {
final myGame = _myGame;
return Scaffold(
appBar: AppBar(
title: const Text('Testing addingOverlay'),
),
body: myGame == null
? const Text('Wait')
: GameWidget<ExampleGame>(
game: myGame,
overlayBuilderMap: {
'PauseMenu': pauseMenuBuilder,
Widget overlayBuilder(DashbookContext ctx) {
return GameWidget<ExampleGame>(
game: ExampleGame()..paused = true,
overlayBuilderMap: const {
'PauseMenu': _pauseMenuBuilder,
},
initialActiveOverlays: const ['PauseMenu'],
),
floatingActionButton: FloatingActionButton(
onPressed: newGame,
child: const Icon(Icons.add),
),
);
}
void newGame() {
setState(() {
_myGame = ExampleGame();
});
}
}
class ExampleGame extends FlameGame with TapDetector {
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(
const Rect.fromLTWH(100, 100, 100, 100),
Paint()..color = BasicPalette.white.color,
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.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() {
if (overlays.isActive('PauseMenu')) {
overlays.remove('PauseMenu');
resumeEngine();
} else {
overlays.add('PauseMenu');
pauseEngine();
}
}
}

View File

@ -4,6 +4,8 @@
- Added `StandardEffectController` class
- Refactored `Effect` class to use `EffectController`, added `Transform2DEffect` class
- 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`
- Alternative implementation of `RotateEffect`, based on `Transform2DEffect`
- Fix `onGameResize` margin bug in `HudMarginComponent`

View File

@ -39,5 +39,10 @@ class GameLoop {
void resume() {
_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();
}
}
}

View File

@ -38,7 +38,7 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver {
game.pauseEngineFn = gameLoop.pause;
game.resumeEngineFn = gameLoop.resume;
if (game.runOnCreation) {
if (!game.paused) {
gameLoop.start();
}

View File

@ -197,14 +197,35 @@ mixin Game on Loadable implements Projector {
);
}
/// Flag to tell the game loop if it should start running upon creation.
bool runOnCreation = true;
bool _paused = false;
/// 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.
void pauseEngine() => pauseEngineFn?.call();
void pauseEngine() {
_paused = true;
pauseEngineFn?.call();
}
/// Resumes the engine game loop execution.
void resumeEngine() => resumeEngineFn?.call();
void resumeEngine() {
_paused = false;
resumeEngineFn?.call();
}
VoidCallback? pauseEngineFn;
VoidCallback? resumeEngineFn;

View 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));
},
);
}