Pausing a Sprite Kit Game, Correctly
When I first added a pause button to my SKScene, I wrote just a single line of code to toggle the pause-state of my scene:
self.paused=!self.paused; //If you think you can get away with this, you are in for a treat, my friend.
While this method “works” to some extent (it correctly pauses/unpauses the physics simulation), it doesn’t address any of the nuances associated with pausing a Sprite Kit game, many of which will lead to spectacular crashes when improperly handled. In order to pause the game in a bullet-proof manner, you should follow these steps:
1) Create dedicated selectors for pausing and unpausing the SKScene.
Link these selectors to the in-game pause button as usual. Note that it is very important to declare a separate “isGamePaused” flag as opposed to relying on SKScene’s self.paused – there are corner-cases where self.paused is not congruent with the game’s actual pause-state.
-(void)pauseGame { _isGamePaused = YES; //Set pause flag to true self.paused = YES; //Pause scene and physics simulation if (_bgMusicOn) { //Pause background music if bg music setting is ON [_bgMusicPlayer pause]; } //Display pause screen etc. } -(void)unpauseGame { _isGamePaused = NO; //Set pause flag to false self.paused = NO; //Resume scene and physics simulation if (_bgMusicOn) { //Resume background music if bg music setting is ON [_bgMusicPlayer play]; } //Hide pause screen etc. }
Note: This tutorial uses the traditional game design style where pausing the game also pauses the background music.
2) Register the SKScene as an observer for important app transitions.
We will handle each event appropriately in the steps to follow.
-(void)registerAppTransitionObservers { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:NULL]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:NULL]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:NULL]; }
Note: You have the option to handle these events from the AppDelegate, but I think it makes more sense to let the SKScene manage its own resources instead of going through a middleman.
3) Handle the applicationWillResignActive event.
This event occurs when the home button is pressed once (bringing up the device home screen) OR when the home button is double-tapped (bringing up the task-switch screen). In either case, the correct behavior should be to pause the game, if it’s not already paused by the user. Nothing fancy so far.
-(void)applicationWillResignActive {
if (!_isGamePaused) { //Pause the game if necessary
[self pauseGame];
}
}
Note: If the user immediately returns to the game from the task-switch screen, the game will never have gone into the background state. The user should be returned to a paused state AS IF they clicked on the in-game pause button. This is a good consistency to have.
3) Handle the applicationDidEnterBackground event.
This event occurs when the home button is pressed once (bringing up the device home screen) OR when the user switches tasks. Note that if this event occurs, it will ALWAYS come after the applicationWillResignActive event. In either case, we will need to handle three “gotchas” associated with putting the game into a background state:
-(void)applicationDidEnterBackground { [_bgMusicPlayer stop]; //A. [[AVAudioSession sharedInstance] setActive:NO error:nil]; //B. self.view.paused = YES; //C. }
A. Quirky iOS behavior does not honor music player “pause” when putting the game into the background. It tries to be a “smart-ass”, fading out the music when the app resigns and fading in the music when the app resumes. The only way to prevent this from happening is to forcefully “stop” the music when the game goes into the background. Otherwise, your background music will resume playing automatically when you return to the game even though the game itself is still in the paused state!
B. AVAudioSession must be turned off when your app is in the background or an EXC_BAD_ACCESS exception will be thrown. There are forums that claim this operation may not always be successful, so they wrap it inside an unbounded while-loop with infinite retries. I have yet to encounter such a problem, but if you insist on adding retry logic, at least have the decency to bound the number of your attempts.
C. The SKView containing your scene must be paused when your game goes into the background or an EXC_BAD_ACCESS exception will be thrown. I believe this is a bug on Apple’s part. There are forums that claim this problem can be avoided if you make your game using storyboard, but I don’t buy that crap.
4) Handle the applicationWillEnterForeground event.
This event occurs when the game is first launched OR when the game is relaunched (either from the home screen or the task-switch screen). We should reverse the steps taken by applicationDidEnterBackground (but keep in mind that applicationDidEnterBackground may not have happened).
-(void)applicationWillEnterForeground { self.view.paused = NO; //Unpause SKView. This is safe to call even if the view is not paused. if (_isGamePaused) { //See note. self.paused = YES; } [[AVAudioSession sharedInstance] setActive:YES error:nil]; //This is technically optional, but included to preserve symmetry. }
Note: Due to a questionable design choice, unpausing the view automatically unpauses the scene (and the physics simulation). Therefore, we must manually pause the scene again, if the game is supposed to be in a paused state.
And there you have it! Feel free to test your game to your heart’s content to convince yourself that this is indeed a bullet-proof solution~
Leave a Reply
You must be logged in to post a comment.