Search code examples
iossprite-kitprotocolsskscene

Re-Assigning SKScene delegate protocol


So I create a delegate protocol and corresponding property in my scene. In my view controller that holds all my scenes I set the scenes delegate to itself so I can call a fullscreen ad(from the view controller) in the menu which is the first game scene called. This works great I can call the method. But after i transition to the level scenes then back to the menu scene calling my method to show fullscreen ad does nothing. Im assuming because the delegate being set to itself has been lost. How can I set the delegate back to itself within the game scene? This is how I do it inside the View controller.M

myScene = [GameScene unarchiveFromFile:@"GameScene"];
((GameScene *)myScene).mySceneDelegate = self; // i need to be able to do this within the scene
myScene.scaleMode = SKSceneScaleModeResizeFill;

// Present the scene.
[skView presentScene:myScene];

How I call the fullScreen method thats in my VC from my menu scene...

[self.mySceneDelegate showFullScreen:self];

...so in my code when I transition from my LevelOne scene I try to reassign the delegate with the following code but it nothing happens

-(void)goBackToMenu{

    GameScene *newscene = [GameScene sceneWithSize:self.size];
    [self.view presentScene:newscene transition:[SKTransition doorsCloseHorizontalWithDuration:1]];
    ((GameScene *)newscene).mySceneDelegate = self;
}

I have also tried setting the property attribute to Strong instead of weak with no luck either

**********************ALL CODE **************************************

More code.. my Viewcontroller.h

@protocol TCAMySceneDelegate;

@interface GameViewController : UIViewController<TCAMySceneDelegate>

@property(nonatomic, weak) id<TCAMySceneDelegate> mySceneDelegate;

@end

my view controller.m

- (void)viewDidLoad
{
    [super viewDidLoad];

    SKView * skView = (SKView *)self.view;

    myScene = [GameScene unarchiveFromFile:@"GameScene"];

    ((GameScene *)myScene).mySceneDelegate = self;

    // Present the scene.
    [skView presentScene:myScene];
}


-(void)showFullScreen:(GameScene *)gameScene{

    NSLog(@"Show Full Screen");

    [revmobFS loadWithSuccessHandler:^(RevMobFullscreen *fs) {
        [fs showAd];
        NSLog(@"Ad loaded");
    } andLoadFailHandler:^(RevMobFullscreen *fs, NSError *error) {
        NSLog(@"Ad error: %@",error);
    } onClickHandler:^{
        NSLog(@"Ad clicked");
    } onCloseHandler:^{
        NSLog(@"Ad closed");
    }];
    [RevMobAds startSessionWithAppID:@"547251235f2a043608a66f1a"
                  withSuccessHandler:^{
                      NSLog(@"Session started with block");
                      // Here, you should call the desired ad unit
                      revmobFS = [[RevMobAds session] fullscreen];
                      [revmobFS showAd];
                      // [RevMobAds session].testingMode = RevMobAdsTestingModeWithAds;
                      revmobFS.delegate = self;
                      [revmobFS loadAd];

                  } andFailHandler:^(NSError *error) {
                      NSLog(@"Session failed to start with block");
                  }];
}

my game scene.h file..

@protocol TCAMySceneDelegate;

@interface GameScene : SKScene

@property (nonatomic, weak) id<TCAMySceneDelegate> mySceneDelegate;

@end

@protocol TCAMySceneDelegate <NSObject>

-(void)showFullScreen:(GameScene *)gameScene;
@end

my game scene.m file..

-(void)didMoveToView:(SKView *)view {

    //Show Ad

    [self.mySceneDelegate showFullScreen:self];

}

now the above showFullScreen: is called but online when the gameScene (my game menu) is presented the first time .. after transitioning to other scenes in the games such as my levels then go back to my menu(gamescene) it never gets called. Hope this is more clear now

how i switch to another scene from my game scene.m

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    SKNode *node = [self nodeAtPoint:location];
    NSLog(@"Level Selected: %@", node.name);
    int x = [node.name intValue];

    switch (x) {
        case 1:{
                LevelOne *newscene = [LevelOne sceneWithSize:self.size];
                [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
            break;
        }
        case 2:{

                LevelTwo *newscene = [LevelTwo sceneWithSize:self.size];
                [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
            }
            break;
        }
        case 3:{
                LevelThree *newscene = [LevelThree sceneWithSize:self.size];
                [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
            }
            break;
        }
//..........

        default:
            break;
    }
}

LevelOne.h code...

#import <SpriteKit/SpriteKit.h>
#import "DrawCanvas.h"
#import "GameScene.h"

@interface LevelOne : SKScene<SKPhysicsContactDelegate>

@end

Solution

  • Personally, I found your description as well as the code you posted a bit lacking in details as well as being a bit confusing. When providing code you will probably get better/quicker help if you provide more detail. For example, which file does goBackToMenu belong to?

    How/in what context is

    [self.mySceneDelegate showFullScreen:self];
    

    called? I would assume that it is from some trigger point in the GameScene. And what is the method signature of showFullScreen?

    And since you don't indicate what type mySceneDelegate is, you're making the readers guess what you mean.

    So trying to decipher what you are asking...

    Let's assume you have some delegate which is called GameSceneDelegate. I am also going to assume you already know SKSceneDelegate exists and are either using it already but just haven't posted that code or don't need to use it.

    Your GameViewController would have it's interface defined as:

    @interface GameViewController : UIViewController<GameSceneDelegate>

    And your GameScene would have a property defined like this:

    @property (nonatomic, weak) id<GameSceneDelegate> mySceneDelegate;
    

    Note you should be using weak here. Because if your GameViewController ever has a strong reference to your GameScene, then you've just created a retain cycle.

    You have a few options of when to set mySceneDelegate. If I know I must have a delegate defined, what I often do is add it as an argument to the initializers. This way it is clear that it must have a delegate. So, given you are using class methods to generate instances, you'd do something like this:

    @interface GameScene : SKScene
    
    @property (nonatomic, weak) id<GameSceneDelegate> mySceneDelegate;
    
    + (instancetype)unarchiveFromFile:(NSString *)file mySceneDelegate:(id<GameSceneDelegate>)mySceneDelegate;
    
    + (instancetype)sceneWithSize:(CGSize)size mySceneDelegate:(id<GameSceneDelegate>)mySceneDelegate;
    
    @end
    

    And you those methods would look like:

    + (instancetype)unarchiveFromFile:(NSString *)file mySceneDelegate:(id<GameSceneDelegate>)mySceneDelegate
    {
        GameScene *scene = [super unarchiveFromFile:file];
    
        scene.mySceneDelegate = mySceneDelegate;
    
        return scene;
    }
    
    + (instancetype)sceneWithSize:(CGSize)size mySceneDelegate:(id<GameSceneDelegate>)mySceneDelegate
    {
        GameScene *scene = [super sceneWithSize:size];
    
        scene.mySceneDelegate = mySceneDelegate;
    
        return scene;
    }
    

    Note to make this work, you'll need to define this in the GameViewController.h (or a separate file for the category if you wish)

    @interface SKScene (Unarchive)
    
    + (instancetype)unarchiveFromFile:(NSString *)file;
    
    @end
    

    The alternative is to immediately set mySceneDelegate right after creating your instance of the scene.

    Realistically, if you are doing this, it should work and your delegate's should stay in tact. FWIW, I am actually doing this in my game. So I know it works.

    Okay, so this is where things are hazy in your question. You indicate the delegate is "lost". The only way the delegate can be "lost" would be if someone explicitly sets it to nil. Or the other possiblity I suppose is your GameScene is being dealloc'd and you don't realize it.

    I'd setup two things to help debug it.

    In GameScene add

    - (void)dealloc
    {
        NSLog(@"I'm being dealloc'd");
    }
    
    - (void)setMySceneDelegate:(id<GameSceneDelegate>)mySceneDelegate
    {
        _mySceneDelegate = mySceneDelegate;
    
        NSLog(@"Setting mySceneDelegate");
    }
    

    So now you can try and determine where it is "getting lost". If any of those print out to the console at the time it is lost, simply set a breakpoint in those methods to see who is calling it.

    One other pieces of info that would also help are

    • What do you mean assigning a delegate to itself? I have no idea what that means.

    • You mentioned you assume the delegate is lost. Why? Do you have evidence or is this purely a guess?

    If you can provide some answers and better detail, we can probably figure out what is going awry.

    Okay, now looking at your code, I think your problem is here in GameScene.m:

    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        /* Called when a touch begins */
        UITouch *touch = [touches anyObject];
        CGPoint location = [touch locationInNode:self];
        SKNode *node = [self nodeAtPoint:location];
        NSLog(@"Level Selected: %@", node.name);
        int x = [node.name intValue];
    
        switch (x) {
            case 1:{
                    LevelOne *newscene = [LevelOne sceneWithSize:self.size];
                    newScene.mySceneDelegate = self.mySceneDelegate;
                    // Or, if you can make it work, newscene = [LevelOne sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                    [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
                break;
            }
            case 2:{
    
                    LevelTwo *newscene = [LevelTwo sceneWithSize:self.size];
                    newScene.mySceneDelegate = self.mySceneDelegate;
                    // Or, if you can make it work, newscene = [LevelOne sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                    [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
                }
                break;
            }
            case 3:{
                    LevelThree *newscene = [LevelThree sceneWithSize:self.size];
                    newScene.mySceneDelegate = self.mySceneDelegate;
                    // Or, if you can make it work, newscene = [LevelOne sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                    [self.view presentScene:newscene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
                }
                break;
            }
    //..........
    
            default:
                break;
        }
    }
    

    What I am doing here is assigning the the new scene's mySceneDelegate to the existing one being used.

    You can clean this code up to be more readable.

    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        /* Called when a touch begins */
        UITouch *touch = [touches anyObject];
        CGPoint location = [touch locationInNode:self];
        SKNode *node = [self nodeAtPoint:location];
        NSLog(@"Level Selected: %@", node.name);
        int x = [node.name intValue];
        GameScene *newScene = nil;
    
        switch (x) {
            case 1:
                newScene = [LevelOne sceneWithSize:self.size];
                // Or, if you can make it work, newScene = [LevelOne sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                break;
            case 2:
                newScene = [LevelTwo sceneWithSize:self.size];
                // Or, if you can make it work, newScene = [LevelTwo sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                break;
            case 3:
                newScene = [LevelThree sceneWithSize:self.size];
                // Or, if you can make it work, newScene = [LevelThree sceneWithSize:size mySceneDelegate:self.mySceneDelegate;
                break;
    
            // other cases..........
    
            default:
                break;
        }
        if (newScene) {
            newScene.mySceneDelegate = self.mySceneDelegate;
            [self.view presentScene:newScene transition:[SKTransition doorsOpenVerticalWithDuration:1]];
        }
    }
    

    For your levels, you would need to do the following:

    #import <SpriteKit/SpriteKit.h>
    #import "DrawCanvas.h"
    #import "GameScene.h"
    
    @interface LevelXYZ : GameScene<SKPhysicsContactDelegate>
    
    @end
    

    Where XYZ is the level (eg. One). By doing this, you inherit mySceneDelegate from GameScene. If for some reason you are insistent that the level scenes are not inheriting from GameScene, then add a base class which holds the delegate. So that would be something like:

    @interface MyBaseScene : SKScene
    
    @property(nonatomic, weak) id<TCAMySceneDelegate> mySceneDelegate;
    
    @end
    

    And then GameScene would be this:

    @interface GameScene : MyBaseScene
    

    And your level scenes would be

    @interface LevelXYZ : MyBaseScene<SKPhysicsContactDelegate>
    

    Note a couple of things I did.

    • Changed newscene to newScene. In iOS, you typically use came case. you were doing it for mySceneDelegate. Consistency is key.

    • Originally declared newScene as nil and assigned as need. Then at the end of the switch, if a newScene exists, assign the delegate and present. This way, you are only doing it in one place. It makes life easier. For example, if you wanted to change the duration for each one, then what? You have to change ever case. Note there is a downfall to this. If you wanted on level to have a special duration, then it doesn't quite work. But you can change it by making the duration and transition type variables.

    BTW, I would strongly advise against having your scene class contain the code that does the actual transition. I would either have it call a delegate to do it or find another way (like using a notification that a scene change is needed). For example, in my current game, I have my view controller be the controller of all scene changes. The advantage of this is you can also call scene transitions from other external forces (and not have your scene be the one that manages it).

    Lastly, you can hopefully see that the only way the issue was discoverable was by presenting the right code. I will agree it is hard to know what code to present, because if there is too much code, then people may not read it. It's a fine line, but some food for thought.