I have a game where I've successfully implemented a pause feature when the home button is pressed. In my View Controller that has the main scene, I pause using:
- (void)appWillEnterBackground{
SKView *skView = (SKView *)self.view;
skView.paused = YES;
bubbleOn=NO; //turns bubble spawn off
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterForeground)
name:UIApplicationDidBecomeActiveNotification
object:NULL];
}
To unpause,
- (void)appWillEnterForeground{
SKView * skView = (SKView *)self.view;
skView.paused=NO;
bubbleOn=YES; Allows recursive method to run until bubbleOn = YES
[NSTimer scheduledTimerWithTimeInterval:slowMo target:scene selector:#selector(spawnNew) userInfo:nil repeats:NO]; //Recursive spawn method
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterBackground)
name:UIApplicationWillResignActiveNotification
object:NULL];}
This works well until my the game ends and a new scene (End Scene) is presented. After the End Scene shows a score, the user can tap again to start again and main scene is presented. The main scene's initWithSize method begins the recursive method spawnNew. If the app goes to the background, the scene pauses and spawnNew stops.
But when the app goes to foreground, the scene does resume, but the spawnNew method does not work. It gets called and outputs a correct NSLog message, but the the method doesn't spawn bubble nodes.
The spawnNew method is in my main scene's implementation:
-(void) spawnNew{
if (bubbleOn==YES){
bubble = [SKSpriteNode spriteNodeWithImageNamed:ballName];
bubble.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:bubble.size.width/2];
...
//bubble properties
...
[self addChild:bubble];
NSLog(#"Spawn!");
[NSTimer scheduledTimerWithTimeInterval:slowMo target:self selector:#selector(spawnNew) userInfo:nil repeats:NO];
return;
} else{
return;
}
I'm out of ideas at this point! Any suggestions?
The easiest way is to get rid of NSTimer. You can use SKAction instead.
In SKScene's initWithSize method:
- (instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
....
__weak GameScene *weakSelf = self;
SKAction *spawnBubble = [SKAction runBlock:^{
[weakSelf spawnNew];
}];
SKAction *wait = [SKAction waitForDuration:slowMo];
[self runAction:[SKAction repeatActionForever:
[SKAction sequence:#[spawnBubble,wait]]]];
}
return self;
}
In this case you don't even need to use bubbleOn variable, as scene stops executing any actions as soon as it's SKView is paused.
Related
Now I am working on a SKScene. I have two ViewController files and I created two scene files which is subview of a SKScene. I know I can perform many SKActions in the scene files. But I want to do some SKAction in the method appEntersBackground: in the file ViewController.m. But it seems the SKActions in this method don't work and I don't know why.
In the ViewController.m file:
-(void)appEntersBackground {
SKAction *action = [SKAction customActionWithDuration:0.1 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
//perform some actions
}
[myNode runAction: action];
}
You cannot perform an SKAction from the viewController. You might however, trigger a method in the scene itself when the application enters the background and perform the necessary operations there.
Paste the appEntersBackground method in the SKScene itself. Then call the following notification in the didMoveToView method.
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appEntersBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
//The rest of your setup goes here
}
-(void)appEntersBackground {
SKAction *action = [SKAction customActionWithDuration:0.1 actionBlock:^(SKNode *node, CGFloat elapsedTime) {
//perform some actions
}
[myNode runAction: action];
}
The method should get called when the application enters the background.
My code for application will Enter Foreground is given below. I am working on the iOS simulator. What I am trying in my code is, when a player clicks home button while game is going on and returns back to the game after sometime, I want the game to be in pause state. Although my code pauses the game, but it does not pause it immediately. That is, when I go to my game again, there is 1 second of movement before everything pauses.
-(void)applicationWillEnterForeground:(UIApplication *)application
{ [[NSNotificationCenter defaultCenter] postNotificationName:#"didEnterForeground" object:self];
}
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(handleEnterFg)
name: #"didEnterForeground"
object: nil];
-(void) handleEnterFg
{
if (gameIsOver== NO)
{
[myTimer pause];
gamePause = YES;
self.scene.view.paused = YES;
}
}
-(void) handleEnterBg
{
if (gameIsOver== NO)
{
[bgPlayer pause];
[self childNodeWithName:#"pauseButton"].zPosition = -10;
[self childNodeWithName:#"playButton"].zPosition = 160;
}
}
Thank you!
You need to pause the game when you receive applicationWillEnterBackground if you want to pause it immediately.
I have huge issue with my newest game for iPhone made with Games by Tutorials book's help.
NOTE : method from SpriteKit- the right way to multitask doesn't work.
So, in my ViewController.m file, I'm defining private variable SKView *_skView.
Then, I do something like this :
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if(!_skView)
{
_skView = [[SKView alloc] initWithFrame:self.view.bounds];
_skView.showsFPS = NO;
_skView.showsNodeCount = NO;
// Create and configure the scene.
SKScene * scene = [MainMenuScene sceneWithSize:_skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[_skView presentScene:scene];
[self.view addSubview:_skView];
}
}
And I have my _skView defined, and everything works just fine.
But, when I interrupt game, it resets its state to initial, so, for example, if I'm currently playing, and somebody calls me, game switches back to main menu. It can't be like this.
Based on the site I mentioned above, I created this :
- (void)applicationWillResignActive:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
view.paused = YES;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
view.paused = NO;
}
But game crashes as soon as it is launched, because second method is called and SKView* view is nil.
Getting it from self.window.rootViewController.view doesn't work.
I also tried to get it from self.window.rootViewController.view.subviews, but it doesn't work either.
I can't define my SKView (in ViewController.m) like this :
SKView * skView = (SKView *)self.view;
because then I have errors with my GameCenterController.
Can someone help me how correctly get actual skView and pause it properly??
You could subscribe to these notifications yourself within the ViewController that manages the SKView. Then you don't have to navigate some weird hierarchy to obtain it.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(willEnterBackground)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(willEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:nil];
- (void)willEnterForeground {
self.skView.paused = NO;
}
- (void)willEnterBackground {
// pause it and remember to resume the animation when we enter the foreground
self.skView.paused = YES;
}
SpriteKit is supposed to clean up and pause all timers when you press the home button.
However, we have found that if you single tap the home button while displaying an active SKView, the app crashes. This occurs even if that view is already paused by the user.
Strangely, if you double tap the home button and move to the multitasking view, everything works fine.
Follow Up Note: The simulator works perfectly in both situations, no crash
Is anyone else seeing this issue with SpriteKit?
Have you found the cause / solution?
I was having the same problem and I solved it manually pausing the root SKView in the ViewController before the app moving to the background:
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the view (already setup as SKView via storyboard)
SKView * skView = (SKView *)self.view;
// Create and configure the scene.
SKScene *scene = [Menu sceneWithSize:CGSizeMake(skView.bounds.size.height, skView.bounds.size.width)];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterBackground)
name:UIApplicationWillResignActiveNotification
object:NULL];
}
- (void)appWillEnterBackground
{
SKView *skView = (SKView *)self.view;
skView.paused = YES;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterForeground)
name:UIApplicationWillEnterForegroundNotification
object:NULL];
}
- (void)appWillEnterForeground
{
SKView * skView = (SKView *)self.view;
skView.paused = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(appWillEnterBackground)
name:UIApplicationWillResignActiveNotification
object:NULL];
}
Run Xcode's analyzer (Project > Analyze) to determine if you have memory management issues in your code.
Run this scenario in the allocations instrument of the Instruments app (Project > Profile), which can report any detected misuse of memory.
We ran into some similar inconsistent behavior with UICollectionView. In that case, instantiating the object via a Storyboard (versus programmatically) resolved the issue.
On a hunch, I tried that this morning and Viola, success!
So, it appears that Storyboards are doing some magic with SKView on creation that is unknown to us and allows them to be properly terminated on Home Button press. Frustrating, but at least it works now.
The problem can be caused by audio, if so you can check the answer here https://stackoverflow.com/a/19283721/1278463
I solved this in a game which is not using audio. Th solution is to pause SKView when entering background:
- (void)applicationDidEnterBackground:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
if (view) {
view.paused = YES;
}
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
SKView *view = (SKView *)self.window.rootViewController.view;
if (view) {
view.paused = NO;
}
}
I'm using the semi-singleton approach for the main game layer/scene of my cocos2d game as shown in the later code.
The objective is to properly restart/recreate this singleton scene using a button in a pause or gameover layers by calling: [[GameLayer sharedGameLayer] restart] method.
The problem is if I use a CCTransition effect for that, and override the GameLayer's dealloc method with sharedGameLayer = nil; line (to ensure the resetting of the static variable), the sharedGameLayer variable stays nil after the first restart (aka. after the first dealloc), so calling the restart method does nothing.
What works with suspicion is not to override the dealloc method at all, but before restarting the scene using replaceScene:, I set the sharedGameLayer to nil.
Question: Is this the right approach for restarting/recreating this semi-singleton class?
Thanks in advance.
Code:
GameLayer.m:
static GameLayer *sharedGameLayer;
#implementation GameLayer
- (id)init
{
NSLog(#"%s", __PRETTY_FUNCTION__);
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if (self = [super initWithColor:ccc4(255, 255, 255, 255) width:[CCDirector sharedDirector].winSize.width
height:[CCDirector sharedDirector].winSize.height])
{
// Set the sharedGameLayer instance to self.
sharedGameLayer = self;
// Set the initial game state.
self.gameState = kGameStateRunning;
// Register with the notification center in order to pause the game when game resigns the active state.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(pauseGame) name:UIApplicationWillResignActiveNotification object:nil];
// Enable touches and multi-touch.
CCDirector *director = [CCDirector sharedDirector];
[director.touchDispatcher addTargetedDelegate:self priority:0 swallowsTouches:YES];
director.view.multipleTouchEnabled = YES;
// Set the initial score.
self.score = 5;
// Create the spiders batch node.
[self createSpidersBatchNode];
// Load the game assets.
[self loadAssets];
// Play Background music.
if (![SimpleAudioEngine sharedEngine].isBackgroundMusicPlaying)
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:#"backgroundmusic.mp3" loop:YES];
// Preload sound effects.
[[SimpleAudioEngine sharedEngine] preloadEffect:#"inbucket.mp3"];
[[SimpleAudioEngine sharedEngine] preloadEffect:#"outbucket.mp3"];
[[SimpleAudioEngine sharedEngine] preloadEffect:#"gameover.mp3"];
// Schdule updates.
[self scheduleUpdate];
[self schedule:#selector(releaseSpiders) interval:0.7];
}
return self;
}
- (void)createSpidersBatchNode
{
NSLog(#"%s", __PRETTY_FUNCTION__);
// BatchNode. (For Animation Optimization)
self.spidersBatchNode = [CCSpriteBatchNode batchNodeWithFile:#"spiderAtlas.png"];
// Spider sprite + BatchNode + animation action.
for (int i = 0; i < 50; i++) {
Spider *spider = [[Spider alloc] init];
spider.spiderSprite.visible = NO;
}
[self addChild:self.spidersBatchNode];
}
- (void)restartGame
{
// Play button pressed sound effect.
[[SimpleAudioEngine sharedEngine] playEffect:#"button.mp3"];
// Nil'ing the static variable.
sharedGameLayer = nil;
// Restart game.
[[CCDirector sharedDirector] replaceScene:[CCTransitionFade transitionWithDuration:1.0 scene:[GameLayer scene]]];
}
+ (CCScene *)scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// Game Layer.
// 'gameLayer' is an autorelease object.
GameLayer *gameLayer = [GameLayer node];
// add gameLayer as a child to scene
[scene addChild: gameLayer];
// HUD Layer.
HUDLayer *hudLayer = [HUDLayer node];
[scene addChild:hudLayer];
gameLayer.hud = hudLayer;
// return the scene
return scene;
}
+ (GameLayer *)sharedGameLayer
{
return sharedGameLayer;
}
- (void)cleanup
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[CCDirector sharedDirector].touchDispatcher removeDelegate:self];
[self stopAllActions];
[self unscheduleAllSelectors];
[self unscheduleUpdate];
[self removeAllChildrenWithCleanup:YES];
self.hud = nil;
[super cleanup];
}
//- (void)dealloc
//{
// sharedGameLayer = nil;
//}
#end
I bet your new game layer is created before the first one deallocates, therefore it sets the static var to nil. Check that sharedGameLayer equals self in dealloc.
Do not use dealloc to set the static variable to nil. Dealloc happens after your object has stopped being used. Never use it for controlling the behaviour of your app.
For all you know, 30 minutes might have passed between when you set sharedGameLayer to nil and when the dealloc method gets called. Or perhaps dealloc will never be called at all.
Your code looks good, just delete the commented out "dealloc" code.
Also, this code:
[[NSNotificationCenter defaultCenter] removeObserver:self];
Should be done in the -dealloc method in addition to your custom -cleanup method. It is perfectly fine to remove an observer twice, if it's already been removed nothing will happen.
Another note, +sharedGameLayer should return a variable of type instancetype and it should be responsible for setting the sharedGameLayer variable. If you sharedGameLayer inside -init, you will run into bugs.
For example:
+ (instancetype)sharedGameLayer
{
if (sharedGameLayer)
return sharedGameLayer;
sharedGameLayer = [[[self class] alloc] init];
return sharedGameLayer;
}
- (id)init
{
if (!(self = [super init]))
return nil;
.
.
.
return self;
}