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,73 +1,53 @@ | |||||||
| 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 _pauseMenuBuilder(BuildContext buildContext, ExampleGame game) { | ||||||
|  |   return Center( | ||||||
|  |     child: Container( | ||||||
|  |       width: 100, | ||||||
|  |       height: 100, | ||||||
|  |       color: const Color(0xFFFF0000), | ||||||
|  |       child: const Center( | ||||||
|  |         child: Text('Paused'), | ||||||
|  |       ), | ||||||
|  |     ), | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
| Widget overlayBuilder(DashbookContext ctx) { | Widget overlayBuilder(DashbookContext ctx) { | ||||||
|   return const OverlayExampleWidget(); |   return GameWidget<ExampleGame>( | ||||||
| } |     game: ExampleGame()..paused = true, | ||||||
|  |     overlayBuilderMap: const { | ||||||
| class OverlayExampleWidget extends StatefulWidget { |       'PauseMenu': _pauseMenuBuilder, | ||||||
|   const OverlayExampleWidget({Key? key}) : super(key: key); |     }, | ||||||
|  |     initialActiveOverlays: const ['PauseMenu'], | ||||||
|   @override |   ); | ||||||
|   _OverlayExampleWidgetState createState() => _OverlayExampleWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _OverlayExampleWidgetState extends State<OverlayExampleWidget> { |  | ||||||
|   ExampleGame? _myGame; |  | ||||||
|  |  | ||||||
|   Widget pauseMenuBuilder(BuildContext buildContext, ExampleGame game) { |  | ||||||
|     return Center( |  | ||||||
|       child: Container( |  | ||||||
|         width: 100, |  | ||||||
|         height: 100, |  | ||||||
|         color: const Color(0xFFFF0000), |  | ||||||
|         child: const Center( |  | ||||||
|           child: Text('Paused'), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @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, |  | ||||||
|               }, |  | ||||||
|               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
	 Erick
					Erick