From c0c900f48f910221192191f856f46ddc6ad013a6 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Tue, 8 Aug 2023 19:50:07 +0100 Subject: [PATCH 1/4] fix: Pause forge2d when backgrounded fix: Pause forge2d when backgrounded chore: temporarily add print statements Revert "chore: temporarily add print statements" This reverts commit 5f672dfbe9e34c0bc45b6e868b61ca0cfa35999f. fix: prevent duplicate `pauseEngine` calls ref: move `pauseWhenBackgrounded` to `FlameGame` fix: remove outdated comment ref: move test to flame package chore: dart format . chore: remove unused import ref: move variables closer to usage ref: remove `pauseWhenBackgrounded` from constructor feat: make pauseWhenBackgrounded true by default docs: mention only working on mobile docs: add doc for pauseWhenBackgrounded flag --- .github/.cspell/gamedev_dictionary.txt | 3 ++ doc/flame/game.md | 18 +++++++++ packages/flame/lib/src/game/flame_game.dart | 40 +++++++++++++++++++ .../flame/lib/src/game/game_render_box.dart | 4 ++ packages/flame/test/game/flame_game_test.dart | 22 ++++++++++ 5 files changed, 87 insertions(+) diff --git a/.github/.cspell/gamedev_dictionary.txt b/.github/.cspell/gamedev_dictionary.txt index c72f6d39566..cf1179a5141 100644 --- a/.github/.cspell/gamedev_dictionary.txt +++ b/.github/.cspell/gamedev_dictionary.txt @@ -24,6 +24,8 @@ arial # name of a typeface arities # plural of arity arity # number of parameters a function takes autofocus # auto focus event +backgrounded # moving the app to the background +backgrounding # moving the app to the background backpressure # strategy to deal with excess flow of data backquote # another word for backtick backtick # the back tick character "`" @@ -39,6 +41,7 @@ coord # coordinate coords # plural of coord deduplication # removal of duplicates easings # Easing functions specify the rate of change of a parameter over time +foregrounded # moving the app to the foreground fullscreen # mode in which a program or app occupies the entire screen with no borders goldens # test files used as reference for Golden Tests hardcoding # putting a value as a literal instead of computing it diff --git a/doc/flame/game.md b/doc/flame/game.md index d4a5d2c093f..51db73c5687 100644 --- a/doc/flame/game.md +++ b/doc/flame/game.md @@ -226,3 +226,21 @@ While the game is paused, it is possible to advanced it frame by frame using the method. It might not be much useful in the final game, but can be very helpful in inspecting game state step by step during the development cycle. + + +### Backgrounding + +The game will be automatically paused when the app is sent to the background, +and resumed when it comes back to the foreground. This behavior can be disabled by setting +`pauseWhenBackgrounded` to `false`. + +```dart +class MyGame extends FlameGame { + MyGame() { + pauseWhenBackgrounded = false; + } +} +``` + +On the current Flutter stable (3.13), this flag is effectively ignored on +non-mobile platforms including the web. diff --git a/packages/flame/lib/src/game/flame_game.dart b/packages/flame/lib/src/game/flame_game.dart index 746ed909da8..59ca8bf5cdc 100644 --- a/packages/flame/lib/src/game/flame_game.dart +++ b/packages/flame/lib/src/game/flame_game.dart @@ -255,4 +255,44 @@ class FlameGame extends ComponentTreeRoot } } } + + /// Whether the game should pause when the app is backgrounded. + /// + /// On the latest Flutter stable at the time of writing (3.13), + /// this is only working on Android and iOS. + /// + /// Defaults to true. + bool pauseWhenBackgrounded = true; + bool _pausedBecauseBackgrounded = false; + + @override + @mustCallSuper + void lifecycleStateChange(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + case AppLifecycleState.inactive: + if (_pausedBecauseBackgrounded) { + resumeEngine(); + } + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + if (pauseWhenBackgrounded && !paused) { + pauseEngine(); + _pausedBecauseBackgrounded = true; + } + } + } + + @override + void pauseEngine() { + _pausedBecauseBackgrounded = false; + super.pauseEngine(); + } + + @override + void resumeEngine() { + _pausedBecauseBackgrounded = false; + super.resumeEngine(); + } } diff --git a/packages/flame/lib/src/game/game_render_box.dart b/packages/flame/lib/src/game/game_render_box.dart index 608561ef748..549b91f48b8 100644 --- a/packages/flame/lib/src/game/game_render_box.dart +++ b/packages/flame/lib/src/game/game_render_box.dart @@ -132,10 +132,14 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { void _bindLifecycleListener() { WidgetsBinding.instance.addObserver(this); + didChangeAppLifecycleState( + WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed, + ); } void _unbindLifecycleListener() { WidgetsBinding.instance.removeObserver(this); + didChangeAppLifecycleState(AppLifecycleState.paused); } @override diff --git a/packages/flame/test/game/flame_game_test.dart b/packages/flame/test/game/flame_game_test.dart index 7a203cc6f98..2404d7a01a6 100644 --- a/packages/flame/test/game/flame_game_test.dart +++ b/packages/flame/test/game/flame_game_test.dart @@ -698,6 +698,28 @@ void main() { }); }); }); + + group('pauseWhenBackgrounded:', () { + testWithFlameGame('true', (game) async { + game.pauseWhenBackgrounded = true; + + game.lifecycleStateChange(AppLifecycleState.paused); + expect(game.paused, true); + + game.lifecycleStateChange(AppLifecycleState.resumed); + expect(game.paused, false); + }); + + testWithFlameGame('false', (game) async { + game.pauseWhenBackgrounded = false; + + game.lifecycleStateChange(AppLifecycleState.paused); + expect(game.paused, false); + + game.lifecycleStateChange(AppLifecycleState.resumed); + expect(game.paused, false); + }); + }); }); } From 3a49b6443d490c16d08948eaf4d92475a16e1c8f Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Mon, 18 Sep 2023 23:18:59 +0100 Subject: [PATCH 2/4] fix: call lifecycleStateChange before onMount --- packages/flame/lib/src/game/game_render_box.dart | 4 ---- packages/flame/lib/src/game/game_widget/game_widget.dart | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/flame/lib/src/game/game_render_box.dart b/packages/flame/lib/src/game/game_render_box.dart index 549b91f48b8..608561ef748 100644 --- a/packages/flame/lib/src/game/game_render_box.dart +++ b/packages/flame/lib/src/game/game_render_box.dart @@ -132,14 +132,10 @@ class GameRenderBox extends RenderBox with WidgetsBindingObserver { void _bindLifecycleListener() { WidgetsBinding.instance.addObserver(this); - didChangeAppLifecycleState( - WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed, - ); } void _unbindLifecycleListener() { WidgetsBinding.instance.removeObserver(this); - didChangeAppLifecycleState(AppLifecycleState.paused); } @override diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index f2fcdf858be..760de0b3516 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -254,6 +254,9 @@ class GameWidgetState extends State> { currentGame = widget.game!; } currentGame.addGameStateListener(_onGameStateChange); + currentGame.lifecycleStateChange( + WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed, + ); _loaderFuture = null; } @@ -262,6 +265,7 @@ class GameWidgetState extends State> { /// `currentGame`'s `onDispose` method will be called; otherwise, it will not. void disposeCurrentGame({bool callGameOnDispose = false}) { currentGame.removeGameStateListener(_onGameStateChange); + currentGame.lifecycleStateChange(AppLifecycleState.paused); currentGame.onRemove(); if (callGameOnDispose) { currentGame.onDispose(); From 653dab720e60c7bccbf017f946cf243bf42a9390 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 21 Sep 2023 18:40:05 +0200 Subject: [PATCH 3/4] Add widget tests --- packages/flame/test/game/flame_game_test.dart | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/packages/flame/test/game/flame_game_test.dart b/packages/flame/test/game/flame_game_test.dart index b7cae2b442c..9212d616c74 100644 --- a/packages/flame/test/game/flame_game_test.dart +++ b/packages/flame/test/game/flame_game_test.dart @@ -9,6 +9,7 @@ import 'package:flame/game.dart'; import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart'; import 'package:flame/src/game/game_render_box.dart'; import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -47,8 +48,8 @@ void main() { await game.ready(); expect(innerGame.canvasSize, closeToVector(Vector2(800, 600))); - expect(innerGame.isLoaded, true); - expect(innerGame.isMounted, true); + expect(innerGame.isLoaded, isTrue); + expect(innerGame.isMounted, isTrue); }); group('components', () { @@ -58,8 +59,8 @@ void main() { final component = Component(); await game.ensureAdd(component); - expect(component.isMounted, true); - expect(game.children.contains(component), true); + expect(component.isMounted, isTrue); + expect(game.children.contains(component), isTrue); }, ); @@ -69,7 +70,7 @@ void main() { final component = _MyAsyncComponent(); await game.ensureAdd(component); - expect(game.children.contains(component), true); + expect(game.children.contains(component), isTrue); expect(component.gameSize, game.size); expect(component.game, game); }, @@ -128,12 +129,12 @@ void main() { await game.add(component); renderBox.gameLoopCallback(1.0); - expect(component.isUpdateCalled, true); + expect(component.isUpdateCalled, isTrue); renderBox.paint( PaintingContext(ContainerLayer(), Rect.zero), Offset.zero, ); - expect(component.isRenderCalled, true); + expect(component.isRenderCalled, isTrue); renderBox.detach(); }, ); @@ -165,7 +166,7 @@ void main() { expect(world.children.length, equals(1)); component.removeFromParent(); game.updateTree(0); - expect(world.children.isEmpty, equals(true)); + expect(world.children.isEmpty, equals(isTrue)); }, ); @@ -175,7 +176,7 @@ void main() { final game = FlameGame(); final world = game.world; final component = Component()..addToParent(world); - expect(game.hasLayout, false); + expect(game.hasLayout, isFalse); await tester.pumpWidget(GameWidget(game: game)); game.update(0); @@ -704,21 +705,63 @@ void main() { game.pauseWhenBackgrounded = true; game.lifecycleStateChange(AppLifecycleState.paused); - expect(game.paused, true); + expect(game.paused, isTrue); game.lifecycleStateChange(AppLifecycleState.resumed); - expect(game.paused, false); + expect(game.paused, isFalse); }); testWithFlameGame('false', (game) async { game.pauseWhenBackgrounded = false; game.lifecycleStateChange(AppLifecycleState.paused); - expect(game.paused, false); + expect(game.paused, isFalse); game.lifecycleStateChange(AppLifecycleState.resumed); - expect(game.paused, false); + expect(game.paused, isFalse); }); + + testWidgets( + 'game is not paused on start', + (tester) async { + final game = FlameGame(); + + await tester.pumpWidget( + GameWidget(game: game), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(game.paused, isFalse); + }, + ); + + testWidgets( + 'game is paused when app is backgrounded', + (tester) async { + final game = FlameGame(); + + await tester.pumpWidget( + MaterialApp( + home: GameWidget(game: game), + ), + ); + + await game.toBeLoaded(); + await tester.pump(); + + expect(game.paused, isFalse); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.paused, + ); + expect(game.paused, isTrue); + WidgetsBinding.instance.handleAppLifecycleStateChanged( + AppLifecycleState.resumed, + ); + expect(game.paused, isFalse); + }, + ); }); }); } From 8ac9626738644d7275371cf0bd976a338bb3cdc4 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Thu, 21 Sep 2023 18:49:18 +0200 Subject: [PATCH 4/4] Remove outer material app --- packages/flame/test/game/flame_game_test.dart | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/flame/test/game/flame_game_test.dart b/packages/flame/test/game/flame_game_test.dart index 9212d616c74..b29c88fafc6 100644 --- a/packages/flame/test/game/flame_game_test.dart +++ b/packages/flame/test/game/flame_game_test.dart @@ -11,7 +11,6 @@ import 'package:flame/src/game/game_render_box.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../components/component_test.dart'; @@ -742,11 +741,7 @@ void main() { (tester) async { final game = FlameGame(); - await tester.pumpWidget( - MaterialApp( - home: GameWidget(game: game), - ), - ); + await tester.pumpWidget(GameWidget(game: game)); await game.toBeLoaded(); await tester.pump();