Wednesday, August 8, 2018

Objective-C Tutorials: Singletons for Gaming Applications

In this tutorial, we build upon the codebase developed in a previous tutorial about Objective-C categories in iOS gaming.   There, we used a category (DesertHero+AnimationsGenerator.h) to supplement the functionality of a game character for our mini-project.  Here, we will refactor the code for the category so that animations are generated via preloaded SKTextureAtlases.  This way, our code will be more memory efficient and faster, since the category methods responsible for generating animations will not have to newly instantiate textures each time an animation is generated.  Instead,  the textures for each animation will have already been instantiated and stored in a singleton by the time we generate the animation.  If you are interested in following along, you can clone or download the source code for the project here, which basically a separate branch from the repo for the original tutorial.
If you don’t remember what the final project looks like, here is a video screenshot as refresher:


Nothing will change in terms of what the final project looks like.  The only difference here will be in the way we refactor our code.  Before we begin, click on the the Assets.xcasssets folder for the project and create several new sprite atlases, each named after a different character animation – that is, “Idle”, “Jump”,”Shoot”,”Run”, and “Walk.”  These folders are technically sprite atlases, but they are also referred to as texture atlases in Apple’s official documentation as well.  In each folder, we will add the images required to build each separate animation.  The final result should look something like what is shown below:





Now that the game assets are stored in the Assets.xcassets folder, let’s create the interface for TextureManager interface.  The interface will include a class variable sharedManager for the singleton instance, as shown below:
TextureManager.h
@import Foundation;
@import SpriteKit;

@interface TextureManager: NSObject

+(TextureManager*)sharedManager;

@property (readonly) SKTextureAtlas* idleTextureAtlas;
@property (readonly) SKTextureAtlas* runTextureAtlas;
@property (readonly) SKTextureAtlas* walkTextureAtlas;
@property (readonly) SKTextureAtlas* shootTextureAtlas;
@property (readonly) SKTextureAtlas* jumpTextureAtlas;


@end

As you can see, we’ve defined readonly properties for texture atlases whose names correspond to the different types of animations that we will generate in our DesertHero class category.   When accessing the texture atlases, it’s not necessary to overwrite them, since we will only be accessing them in the category to retrieve individual texture that will be used to generate animations.
In our implementation file, however, we will override the properties defined in the header interface in our class extension and make the properties readwrite so that we can assign each of the preloaded texture atlases to its corresponding  property in extension.
#import <Foundation/Foundation.h>
#import "TextureManager.h"

@interface TextureManager()


@property (readwrite) SKTextureAtlas* idleTextureAtlas;
@property (readwrite) SKTextureAtlas* runTextureAtlas;
@property (readwrite) SKTextureAtlas* walkTextureAtlas;
@property (readwrite) SKTextureAtlas* shootTextureAtlas;
@property (readwrite) SKTextureAtlas* jumpTextureAtlas;


@end

Since this is a singleton, at the top of our implementation section, we will define a static variable to represent the singleton instance, and our implementation of the sharedManager class variable will check for nil and then instantiate the sharedManager via the initializer shown below.  Once the sharedManager has a TextureManager instance assigned to it, the same instance will be returned every time the sharedManager class variable is used.  Also, note that the initializer for the TextureManager class is not included in the interface in the TextureManager.h file and is only used in the implementation of the sharedManager class variable, ensuring that the singleton instance is only instantiated once.
@implementation TextureManager

static TextureManager* sharedManager = nil;

+(TextureManager*)sharedManager{
    
    if(sharedManager == nil){
        sharedManager = [[super allocWithZone:NULL] init];
    }
    
    return sharedManager;
}

-(instancetype)init{
    
    self = [super init];
    
    if(self){
        
        _idleTextureAtlas = [SKTextureAtlas atlasNamed:@"Idle"];
        _runTextureAtlas = [SKTextureAtlas atlasNamed:@"Run"];
        _walkTextureAtlas = [SKTextureAtlas atlasNamed:@"Walk"];
        _shootTextureAtlas = [SKTextureAtlas atlasNamed:@"Shoot"];
        _jumpTextureAtlas = [SKTextureAtlas atlasNamed:@"Jump"];

    }
    
    return self;
}


@end

In the implementation of the TextureManager initializer, we instantiate texture atlases for each of the texture atlases in our Assets.xcassets folder by calling atlasNamed: class function on the SKTextureAtlas class and assigning the resulting texture atlas to its corresponding instance variable in the texture manager class.
Now we return to the implementation of the DesertHero+AnimationsGenerator.m file, where we previously implemented the animation generator methods as show below:
-(SKAction *)generateIdleAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction* idleAction;
    
    switch (orientation) {
        case LEFT:
            idleAction = [SKAction animateWithTextures:@[
             [SKTexture textureWithImageNamed:@"IdleLeft_000"],
             [SKTexture textureWithImageNamed:@"IdleLeft_001"],
             [SKTexture textureWithImageNamed:@"IdleLeft_002"],
             [SKTexture textureWithImageNamed:@"IdleLeft_003"],
             [SKTexture textureWithImageNamed:@"IdleLeft_004"],
             [SKTexture textureWithImageNamed:@"IdleLeft_005"],
             [SKTexture textureWithImageNamed:@"IdleLeft_006"],
             [SKTexture textureWithImageNamed:@"IdleLeft_007"],
             [SKTexture textureWithImageNamed:@"IdleLeft_008"],
             [SKTexture textureWithImageNamed:@"IdleLeft_009"],

             ] timePerFrame:kIdleActionDuration];
            break;
        case RIGHT:
            idleAction = [SKAction animateWithTextures:@[
             [SKTexture textureWithImageNamed:@"IdleRight_000"],
             [SKTexture textureWithImageNamed:@"IdleRight_001"],
             [SKTexture textureWithImageNamed:@"IdleRight_002"],
             [SKTexture textureWithImageNamed:@"IdleRight_003"],
             [SKTexture textureWithImageNamed:@"IdleRight_004"],
             [SKTexture textureWithImageNamed:@"IdleRight_005"],
             [SKTexture textureWithImageNamed:@"IdleRight_006"],
             [SKTexture textureWithImageNamed:@"IdleRight_007"],
             [SKTexture textureWithImageNamed:@"IdleRight_008"],
             [SKTexture textureWithImageNamed:@"IdleRight_009"],

                 ] timePerFrame:kIdleActionDuration];
            break;
        case SYMMETRICAL:
            break;
        default:
            break;
    }
    
    SKAction* idleAnimation = [SKAction repeatActionForever:idleAction];
    
    return idleAnimation;
    
}

As you can see, the animation generator function require that the SKTextureinstances required to produce an array of SKTexture objects be newly instantiated each time the generator function is called.
Now we will modify the implementation file so that we include a class extension at the top.  Inside the class extension, we will define a readonly property for our texture manager singleton, as shown below:
DesertHero+AnimationsGenerator.m
@interface DesertHero()


@property (readonly) TextureManager* sharedManager;

@end

Then, in the implementation section, we implement the sharedManagerproperty by returning the sharedManager singleton, as shown below:
-(TextureManager *)sharedManager{
    
    return [TextureManager sharedManager];
}

Now that we can access our texture manager singleton access via a readonly computer property, we refactor each of the animation generator methods so that they call the texture manager singleton to retrieve each of the SKTexture objects required to build their corresponding animations:
-(SKAction *)generateIdleAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction* idleAction;
    
    switch (orientation) {
        case LEFT:
            idleAction = [SKAction animateWithTextures:@[
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_000"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_001"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_002"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_003"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_004"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_005"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_006"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_007"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_008"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleLeft_009"],

             ] timePerFrame:kIdleActionDuration];
            break;
        case RIGHT:
            idleAction = [SKAction animateWithTextures:@[
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_000"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_001"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_002"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_003"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_004"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_005"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_006"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_007"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_008"],
             [[self.sharedManager idleTextureAtlas] textureNamed:@"IdleRight_009"],

                 ] timePerFrame:kIdleActionDuration];
            break;
        case SYMMETRICAL:
            break;
        default:
            break;
    }
    
    SKAction* idleAnimation = [SKAction repeatActionForever:idleAction];
    
    return idleAnimation;
    
}


-(SKAction *)generateRunAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getRunAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:@[
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_000"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_001"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_002"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_003"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_004"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_005"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_006"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_007"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_008"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunLeft_009"],

       ]
      
      andWithRightOrientationTexture:@[
      
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_000"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_001"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_002"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_003"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_004"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_005"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_006"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_007"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_008"],
           [[self.sharedManager runTextureAtlas] textureNamed:@"RunRight_009"],



      ] andWithSymmetricOrientationTextures: nil
     andWithTimePerFrame:kRunActionDuration];
    
    return getRunAnimation(orientation);
    
}

-(SKAction *)generateJumpAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getJumpAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:@[
        
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_000"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_001"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_002"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_003"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_004"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_005"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_006"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_007"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_008"],
    [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpLeft_009"],

        ] andWithRightOrientationTexture:@[
       
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_000"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_001"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_002"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_003"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_004"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_005"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_006"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_007"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_008"],
   [[self.sharedManager jumpTextureAtlas] textureNamed:@"JumpRight_009"],
       ] andWithSymmetricOrientationTextures:nil andWithTimePerFrame:kJumpActionDuration];
    
    return getJumpAnimation(orientation);
}



-(SKAction *)generateShootAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getShootAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:
  @[
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_000"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_001"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_002"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_003"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_004"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_005"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_006"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_007"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_008"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootLeft_009"],

    ]
    andWithRightOrientationTexture:
  @[
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_000"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_001"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_002"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_003"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_004"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_005"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_006"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_007"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_008"],
    [[self.sharedManager shootTextureAtlas] textureNamed:@"ShootRight_009"],



    ]
   andWithSymmetricOrientationTextures:nil
   andWithTimePerFrame:kShootActionDuration];
    
    return getShootAnimation(orientation);
    
}



-(SKAction *)generateWalkAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getWalkAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:
  @[
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_000"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_001"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_002"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_003"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_004"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_005"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_006"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_007"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_008"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkLeft_009"],


    ]
   andWithRightOrientationTexture:
  @[
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_000"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_001"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_002"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_003"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_004"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_005"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_006"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_007"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_008"],
    [[self.sharedManager walkTextureAtlas] textureNamed:@"WalkRight_009"]

    ]
                                                                
  andWithSymmetricOrientationTextures:nil andWithTimePerFrame:kWalkActionDuration];
    
    return getWalkAnimation(orientation);
    
};

-(SKAction*(^)(DesertCharacterOrientation))getAnimationGeneratorWithLeftOrientationTextures:(NSArray<SKTexture*>*)leftOrientationTextures andWithRightOrientationTexture:(NSArray<SKTexture*>*)rightOrientationTextures andWithSymmetricOrientationTextures:(NSArray<SKTexture*>*)symmetricOrientationTextures andWithTimePerFrame:(NSTimeInterval)timePerFrame{
    
    return ^(DesertCharacterOrientation orientation){
        
        SKAction* baseAction;
        
        switch (orientation) {
            case LEFT:
                baseAction =  [SKAction animateWithTextures:leftOrientationTextures timePerFrame:timePerFrame];
                break;
            case RIGHT:
                baseAction =  [SKAction animateWithTextures:rightOrientationTextures timePerFrame:timePerFrame];
                break;
            case SYMMETRICAL:
                baseAction =  [SKAction animateWithTextures:symmetricOrientationTextures timePerFrame:timePerFrame];
                break;
            default:
                break;
        }
        
        SKAction* repeatingAnimation = [SKAction repeatActionForever:baseAction];
        
        return repeatingAnimation;
        
        
    };
}


Our refactoring of the original codebase has now allowed us to generate animations much more efficiently. Even though the final result is the same, the difference will become obvious as the number of textures and animations in our project increases.

No comments:

Post a Comment