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. | ||||
|  | ||||
| # 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 | ||||
|  | ||||
| @ -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); | ||||
| } | ||||
|  | ||||
							
								
								
									
										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: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(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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` | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										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