Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Pause game when backgrounded #2642

Merged
merged 5 commits into from
Sep 21, 2023
Merged

Conversation

adil192
Copy link
Contributor

@adil192 adil192 commented Aug 8, 2023

Description

When the game is backgrounded and then resumed, the update function is called with a very large dt time step.

This might cause the physics engine to hit the maximum translation per timestep, causing moving bodies to slow down to almost-zero velocities.

This PR makes the game pause if the game is backgrounded. If you'd like to change this behaviour, set the pauseWhenBackgrounded flag to true (default) or false.

class MyGame extends FlameGame {
  MyGame() {
    pauseWhenBackgrounded = false;
  }
}

This is currently not working on the web, see flutter/flutter#53107.

You can view previous discussion about this here: flame-engine/forge2d#84.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • [-] I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • [-] No, this PR is not a breaking change.

Migration instructions

If you override the lifecycleStateChange method in your Game class, you must now call super.lifecycleStateChange.

If you'd like the game not to pause when backgrounded, set the pauseWhenBackgrounded flag in your Game class to false.

Related Issues

Closes flame-engine/forge2d#84

Copy link
Member

@spydon spydon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be done like this directly in the engine (it's also a breaking change), the user should instead handle pausing the engine on events when the app is backgrounded. Not sure if there are any such events to react to when switching browser tabs though.

@adil192
Copy link
Contributor Author

adil192 commented Aug 8, 2023

Should this not be the default out-of-the-box behaviour though?
If the physics engine can't handle time steps of multiple seconds, then the sensible default is to avoid such time steps

@spydon
Copy link
Member

spydon commented Aug 8, 2023

Should this not be the default out-of-the-box behaviour though?
If the physics engine can't handle time steps of multiple seconds, then the sensible default is to avoid such time steps

That depends on the rest of your settings though, and with a zoom it should very rarely need this except when backgrounded or having massive lags. We should follow how box2d handles it.

The better solution that solves most of these problems is: #2613

@adil192 adil192 changed the title fix: Pause forge2d when backgrounded fix!: Pause forge2d when backgrounded Aug 8, 2023
@adil192
Copy link
Contributor Author

adil192 commented Aug 8, 2023

with a zoom it should very rarely need this except when backgrounded or having massive lags

But an app being backgrounded is not rare at all. The flutter package should handle a common event in a flutter app.

The better solution that solves most of these problems is: #2613

I would argue that this doesn't solve this problem with backgrounding. Our previous discussion in flame-engine/forge2d#84 was about hitting the speed limit with fast bodies, but this PR is only concerned with backgrounding.

Even if we raise the speed limit with some unit conversion like zoom or a meter-to-pixels constant, the app being backgrounded for a few seconds will still hit any speed limit and break expected behaviour.

@adil192
Copy link
Contributor Author

adil192 commented Aug 8, 2023

Perhaps we could use a getter for maxDt instead of a constant, so when devs extend the Forge2DGame they can opt-in to pause the simulation when backgrounded like this:

class MyGame extends Forge2DGame {
  @override
  double get maxDt => 0.5;
}

or even like this:

class MyGame extends Forge2DGame {
  MyGame(): super(
    pauseWhenBackgrounded: true,
  );
}

@spydon
Copy link
Member

spydon commented Aug 8, 2023

But an app being backgrounded is not rare at all. The flutter package should handle a common event in a flutter app.

This covers a lot more cases than just backgrounding an app though.
A PR that pauses the physics engine when an app is backgrounded would be accepted, but this throws away timesteps as default which should not be the case for a physics engine.

@spydon
Copy link
Member

spydon commented Aug 8, 2023

Something like this should be used: https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver/didChangeAppLifecycleState.html

I'm not sure if that would work for web though.

@adil192
Copy link
Contributor Author

adil192 commented Aug 8, 2023

I'm unsure if the AppLifecycleState would work in this scenario, since I think the update method is called after the app is foregrounded again. I need to test it to make sure.
If I'm correct about that, I'll need to modify the code to only restore AppLifecycleState.resumed after the first discarded update

@spydon
Copy link
Member

spydon commented Aug 9, 2023

I'm unsure if the AppLifecycleState would work in this scenario, since I think the update method is called after the app is foregrounded again. I need to test it to make sure. If I'm correct about that, I'll need to modify the code to only restore AppLifecycleState.resumed after the first discarded update

I'm not sure if I understand you correctly here, but you are not the one setting the AppLifecycleState, you get that from overriding didChangeAppLifecycleState.

EDIT: Sorry, missed that you already pushed an update!

@adil192 adil192 marked this pull request as ready for review August 12, 2023 01:12
@adil192
Copy link
Contributor Author

adil192 commented Aug 13, 2023

Did it work in the browser?

Nope :(
Running this code

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print('AppLifecycleState: $state');
    super.didChangeAppLifecycleState(state);
  }

tells me that didChangeAppLifecycleState isn't called at all when switching tabs.

It runs as expected on Android though, so I'm guessing this is a Flutter bug since it's easy to detect tab focus with js.

I/flutter (28489): AppLifecycleState: AppLifecycleState.inactive
I/flutter (28489): AppLifecycleState: AppLifecycleState.paused
I/flutter (28489): update: dt = 0.008337, isAppInForeground = false
I/FA-Ads  (28489): Application backgrounded at: timestamp_millis: 1691897854812
I/flutter (28489): AppLifecycleState: AppLifecycleState.resumed
I/flutter (28489): update: dt = 2.482243, isAppInForeground = true

Edit: Just realised that this means isAppInForeground is true when it shouldn't be. I'm thinking I should make an enum like

enum ForegroundState {
  background,
  foregroundFirstFrame,
  foreground,
}

@spydon
Copy link
Member

spydon commented Aug 18, 2023

Edit: Just realised that this means isAppInForeground is true when it shouldn't be. I'm thinking I should make an enum like

I didn't follow, in which case is the app in the background but with the boolean set to true?

@adil192
Copy link
Contributor Author

adil192 commented Aug 18, 2023

I didn't follow, in which case is the app in the background but with the boolean set to true?

Yes. What happened was:

  1. I backgrounded the app. This sets isAppInForeground to false.
  2. An update was ran while the app was still backgrounded, which incorrectly set isAppInForeground back to true.
  3. I reopened the app.
  4. An update ran with a dt of 2.482243 seconds because the app thinks it has been in the foreground.

packages/flame_forge2d/lib/forge2d_game.dart Outdated Show resolved Hide resolved
packages/flame/lib/src/game/game.dart Outdated Show resolved Hide resolved
@adil192
Copy link
Contributor Author

adil192 commented Aug 24, 2023

This private method should use pauseEngine and resumeEngine instead of using an early return.
Maybe foregroundedFirstUpdate won't be a problem if those two methods are used instead either?

Testing this on Android yields:

I/flutter (20126): update dt: 0.041648
I/flutter (20126): update dt: 0.499768
I/FA-Ads  (20126): Application backgrounded at: timestamp_millis: 1692885388255
I/flutter (20126): update dt: 2.497637
I/flutter (20126): update dt: 0.008329
I/flutter (20126): update dt: 0.008326

which means I need to add foregroundedFirstUpdate (foregrounding) back in

@spydon
Copy link
Member

spydon commented Aug 24, 2023

which means I need to add foregroundedFirstUpdate (foregrounding) back in

Probably better to modify how pause/resume works instead, that behaviour doesn't look like it would be intentional

@spydon
Copy link
Member

spydon commented Sep 8, 2023

Probably better to modify how pause/resume works instead, that behaviour doesn't look like it would be intentional

Did you have any look at this @adil192?

@spydon spydon changed the title fix!: Pause forge2d when backgrounded fix!: Pause game when backgrounded Sep 8, 2023
@spydon
Copy link
Member

spydon commented Sep 11, 2023

Added a test in #2731 that makes sure that dt isn't accumulating meanwhile a game is paused with the pauseEngine method.

@adil192
Copy link
Contributor Author

adil192 commented Sep 12, 2023

No sorry I've been quite busy recently

@adil192 adil192 force-pushed the adil192-patch-1 branch 2 times, most recently from 5fc5e4c to 5f672df Compare September 12, 2023 17:06
@adil192
Copy link
Contributor Author

adil192 commented Sep 12, 2023

With some extra logging (and rebasing the branch), it seems everything is in order.
In the previous log, I must have forgotten to set pauseWhenBackgrounded to true in the constructor.

I/flutter (22999): update dt: 0.008343
I/flutter (22999): update dt: 0.008343
I/flutter (22999): update dt: 0.008344
I/flutter (22999): update dt: 0.00834
I/flutter (22999): lifecycleStateChange: AppLifecycleState.hidden
I/flutter (22999): Pausing engine
I/flutter (22999): lifecycleStateChange: AppLifecycleState.paused
I/flutter (22999): Pausing engine
I/FA-Ads  (22999): Application backgrounded at: timestamp_millis: 1694538751731
I/flutter (22999): lifecycleStateChange: AppLifecycleState.hidden
I/flutter (22999): Pausing engine
I/flutter (22999): lifecycleStateChange: AppLifecycleState.inactive
I/flutter (22999): Resuming engine
I/flutter (22999): update dt: 0.0
I/flutter (22999): update dt: 0.008329
I/flutter (22999): update dt: 0.008342
I/flutter (22999): update dt: 0.008331
I/flutter (22999): update dt: 0.008316
I/flutter (22999): update dt: 0.008332
I/flutter (22999): lifecycleStateChange: AppLifecycleState.resumed
I/flutter (22999): update dt: 0.058368
I/flutter (22999): update dt: 0.008339

@adil192 adil192 marked this pull request as ready for review September 12, 2023 17:18
@spydon
Copy link
Member

spydon commented Sep 14, 2023

@adil192 did you try this on iOS?

@adil192
Copy link
Contributor Author

adil192 commented Sep 14, 2023

@adil192 did you try this on iOS?

I haven't, no. I don't have access to a macOS/iOS device currently

@spydon
Copy link
Member

spydon commented Sep 14, 2023

@adil192 did you try this on iOS?

I haven't, no. I don't have access to a macOS/iOS device currently

Alright, we'll have a test on those then. Which platforms have you tried?

@adil192
Copy link
Contributor Author

adil192 commented Sep 14, 2023

Alright, we'll have a test on those then. Which platforms have you tried?

Just Android at the moment. I can test other platforms if you want me to

@spydon
Copy link
Member

spydon commented Sep 14, 2023

Just Android at the moment. I can test other platforms if you want me to

Please do, I guess you at least have access to one desktop platform and web too?
Would be interesting to see if this also would work if you switch tabs for example, or minimize the application on desktop.

@spydon
Copy link
Member

spydon commented Sep 15, 2023

Alright we had a chat about this, let's make it a breaking change and set it to true by default. :)

@adil192
Copy link
Contributor Author

adil192 commented Sep 16, 2023

This doesn't work on Windows when you minimise an application, it still updates as if it was foregounded. I assume that's another implementation that Flutter needs.

The only lifecycleStateChanges are from when you dispose of the GameWidget widget.

flutter: lifecycleStateChange: AppLifecycleState.resumed # opened game with play button
flutter: lifecycleStateChange: AppLifecycleState.paused # pressed back button
flutter: lifecycleStateChange: AppLifecycleState.resumed # pressed the play button again
# no lifecycleStateChange events if you minimise the window

The web implementation is similarly missing, see flutter/flutter#53107.
I'm not sure if there is a corresponding issue for Windows/other desktop platforms

I'm guessing that AppLifecycleState is only supported on Android and iOS. Do you still want to make pauseWhenBackgrounded true by default?

@spydon
Copy link
Member

spydon commented Sep 16, 2023

I'm guessing that AppLifecycleState is only supported on Android and iOS. Do you still want to make pauseWhenBackgrounded true by default?

Yeah, we should mention it in the docs though.

@spydon
Copy link
Member

spydon commented Sep 17, 2023

Some small issues that needs to be fixed:

  1. Add Backgrounding to the dictionary (probably update the branch first, it might already be added)
  2. Run dart format .
  3. Make sure no lines in the markdown are longer than 100 chars and 2 empty lines above titles.

doc/flame/game.md:236 MD022/blanks-around-headings/blanks-around-headers Headings should be surrounded by blank lines [Expected: 2; Actual: 1; Above] [Context: "### Backgrounding"]
doc/flame/game.md:249:101 MD013/line-length Line length [Expected: 100; Actual: 113]

@adil192
Copy link
Contributor Author

adil192 commented Sep 17, 2023

probably update the branch first, it might already be added

I will do this shortly!

@spydon spydon changed the title feat: Optionally pause game when backgrounded feat!: Optionally pause game when backgrounded Sep 18, 2023
@spydon
Copy link
Member

spydon commented Sep 18, 2023

It seems to somehow break the lifecycle according to one of the lifecycle tests 🤔

fix: Pause forge2d when backgrounded

chore: temporarily add print statements

Revert "chore: temporarily add print statements"

This reverts commit 5f672df.

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
@adil192
Copy link
Contributor Author

adil192 commented Sep 18, 2023

It seems to somehow break the lifecycle according to one of the lifecycle tests 🤔

With some added events:

Expected: [
            'onGameResize',
            'onLoad',
            'onMount',
            'update',
            'render',
            'update',
            'onRemove',
            'onDispose',
            'onGameResize',
            'onMount',
            'update', // <-- this is missing
            'render'
          ]
  Actual: [
            'onGameResize',
            'onLoad',
            'onMount',
            'update',
            // 'lifecycleStateChange(AppLifecycleState.resumed)',
            'render',
            'update',
            // 'lifecycleStateChange(AppLifecycleState.paused)',
            'onRemove',
            'onDispose',
            'onGameResize',
            'onMount',
            // 'lifecycleStateChange(AppLifecycleState.resumed)',
            'render'
          ]

It looks like the first update is discarded because of the use of pauseEngine and resumeEngine. Are you happy for me to just remove the expected 'update', line?

@spydon
Copy link
Member

spydon commented Sep 18, 2023

It looks like the first update is discarded because of the use of pauseEngine and resumeEngine. Are you happy for me to just remove the expected 'update', line?

Unfortunately not, we need to be calling update before render when coming back from being unmounted.

@adil192 adil192 changed the title feat!: Optionally pause game when backgrounded feat!: Pause game when backgrounded Sep 18, 2023
@adil192 adil192 marked this pull request as ready for review September 18, 2023 22:28
@adil192 adil192 requested a review from spydon September 18, 2023 22:28
@@ -254,6 +254,9 @@ class GameWidgetState<T extends Game> extends State<GameWidget<T>> {
currentGame = widget.game!;
}
currentGame.addGameStateListener(_onGameStateChange);
currentGame.lifecycleStateChange(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's pretty weird to call these manually since these state changes should be coming from the framework.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense for the game to act identically to if the app is backgrounded when really it was just unmounted.
This makes the link between "backgrounding the app" and "backgrounding the game (widget)"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus as an added bonus, it makes the behaviour more consistent on non-mobile platforms since the game will be paused when the widget is disposed of

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean, it feels like the state machine could become inconsistent with the one on Flutter's side though, which might confuse developers and be bug prone.

@@ -698,6 +698,28 @@ void main() {
});
});
});

group('pauseWhenBackgrounded:', () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests where it is using a widget and not calling lifecycleStateChange manually? There should be some other widget tests that you can take inspiration from.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some tests, so that we can get this PR into the release.

@spydon spydon enabled auto-merge (squash) September 21, 2023 16:42
@spydon spydon merged commit 521e56b into flame-engine:main Sep 21, 2023
7 checks passed
@adil192 adil192 deleted the adil192-patch-1 branch September 26, 2023 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants