1

I've implemented a "pause menu" in my game. When the user taps the pause button on the top left corner of the screen, I run the following code:

- (void) pauseGame
{
    // Blur the game's contents:
    [_effectNode setShouldEnableEffects:YES];

    // Apply cover:
    [_pauseLayer addChild:_pauseCover]; // (_pauseLayer sits just above _effectNode)

    // Start from fully transparent...
    [_pauseCover setAlpha:0.0f]; // Start from fully transparent...

    // ...and animate to near-opaque:
    [_pauseCover runAction:[SKAction fadeAlphaTo:0.85f duration:0.125f]];
}

to un-pause, I do this:

- (void) unpauseGame
{
    // Un-blur game contents
    [_effectNode setShouldEnableEffects:NO];

    // Fade cover out
    [_pauseLayer runAction:[SKAction fadeAlphaTo:0.0f
                                        duration:0.125f]
                completion:^(void){
                    // The following lines are executed, but still 
                    // the button below doesn't receive any more touches

                    [_pauseLayer removeAllChildren];// Neither
                    [_pauseCover removeFromParent]  // of these
                    [_pauseLayer setHidden:YES];    // makes a difference

                }];
}

After un-pausing, the node hierarchy is exactly the same as before (I logged it). However, the pause button, once covered by _pauseLayer (of type SKNode) and its child _pauseCover (of type SKSpriteNode) no longer receives touch input (the game scene itself, does).

If instead of removing the cover on the fade action's completion block, I do it right away, or using -performSelector:withObject:afterDelay:, like this:

- (void) unpauseGame
{
    // Un-blur game contents:
    [_effectNode setShouldEnableEffects:NO];

    // Fade cover out to fully transparent:
    [_pauseLayer runAction:[SKAction fadeAlphaTo:0.0f
                                        duration:0.125f]];

    [_pauseLayer performSelector:@selector(removeAllChildren) 
                      withObject:nil
                      afterDelay:0.25]; // a bit longer than fade, just in case
}

...Then, after resuming, the button works just as before... I know SKSpriteNode instances block touch events (even with setUserInteractionEnabled:NO), but the sprites are no longer there! Puzzling!

NOTE I must stress that the completion block does get executed (breakpoints stop there), and logging the removed children after that gives the proper count (0). However, the pause button (a sprite subclass that implements the -touches*: withEvent: methods) no longer receives touches.


SOLUTION: I never quite got to accomplish what I wanted: Have a bunch of "placeholder" nodes acting as layers (levels) persisting at fixed depths, and then add/remove children to them as necessary (in this case, the cover sprite/dialogs). I wanted to implement it this way, because SKNode does not have a -insertChild:aboveChild: or insertChild:belowChild: methods, and getting the target index and inserting there is a bit bug prone/confusing (in which direction does the existing node get "pushed"?).

Bit nevertheless, I gave in and decided to add/remove the parent layers on demand, just keeping track of the order (First-in-last-out, to preserve the intended layer order).

Enough talk, the code I settled for is:

#define GameSceneDefaultSingleFadeDuration 0.125f

- (void) pause:(id) sender
{
    [_effectNode setShouldEnableEffects:YES];

    [self addChild:_pauseLayer];
    [_pauseLayer addChild:_pauseCover];
    [_pauseCover setAlpha:0.0f];

    [_pauseCover runAction:[SKAction fadeAlphaTo:0.85
                                        duration:GameSceneDefaultSingleFadeDuration]
                completion:^(void){
                }];

    [self addChild:_dialogLayer];
    [_dialogLayer addChild:_pauseDialog];


    for (Button* button in [_pauseDialog children]) {
        [button setAlpha:0.0f];
        [button runAction:[SKAction fadeAlphaTo:1.0f
                                       duration:GameSceneDefaultSingleFadeDuration]];
    }
}

- (void) resume:(id) sender
{
    // Unblur, fade white cover out and remove

    [_effectNode setShouldEnableEffects:NO];

    [_dialogLayer runAction:[SKAction fadeAlphaTo:0.0f
                                         duration:GameSceneDefaultSingleFadeDuration]
                 completion:^(void){

                     [_dialogLayer removeAllChildren];
                     [_dialogLayer setAlpha:1.0f];
                     [_dialogLayer removeFromParent];
                 }];

    [_pauseCover runAction:[SKAction fadeAlphaTo:0.0f
                                        duration:GameSceneDefaultSingleFadeDuration]
                completion:^(void){

                    [_pauseCover removeFromParent];
                    [_pauseLayer removeFromParent];
                }];
}

(_pauseLayer goes on top of the blurred game content, and _dialogLayer still on top of that, hosting the different dialogs: "Paused", "Confirm Quit", "Confirm Restart").

Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189
  • 1
    Test for any "ghost" layers, for example check any code path to make sure the _pauseLayer is only added once and not multiple times. Secondly I could imagine a retain cycle being an issue, because the block retains the _pauseLayer. Perhaps instead of running the code in the completion block do a performSelectorOnMainThread:afterDelay: that calls a selector performing the cleanup. – CodeSmile Apr 07 '14 at 15:55
  • Thanks... I already scraped all this code and made something different, that seems to be working. Will be posting the code soon. – Nicolas Miari Apr 08 '14 at 00:54
  • I have a method that logs the entire node hierarchy, from SKScene down. It is a recursive method with a `node` parameter and a `depth` parameter (to tab-indent). The outputs from before and after pausing/resuming the game are identical (save for timestamps), so there should be no ghost layer anywhere... – Nicolas Miari Apr 08 '14 at 00:58
  • 1
    SpriteKit-Quicklook can help debug these things: https://github.com/KoboldKit/SpriteKit-QuickLook – CodeSmile Apr 08 '14 at 08:50

0 Answers0