Wednesday, August 8, 2018

Objective-C Tutorials: Categories in Gaming Applications

Objective-C categories are often used to provide additional methods for supplementing the functionality of a given class.  When the header file for the category is imported in the implementation file for a given class, all of the supplementary category methods become available in the main implementation file.  The project for this tutorial AnimationsTester (*source code available here),  will provide a demonstration of how categories can be useful for providing specific functionalities for game characters.   Clicking on a menu button in the AnimationsController for the project will activate different animations, which are then displayed in an embedded SKView (i.e. SpriteKit view).  Above the embedded SKView is a segmented control that is used to toggle the orientation of the animation (i.e. switch between left-facing and right-facing animations).
* Please be aware that the game assets for this project are not available due to copyright concerns.  However, the source code can easily be adapted for the game assets specific for your project.




In order to begin, create a header file (DesertHero.h) that will define the public interface for our main game character:
@import SpriteKit;

#import "AnimationType.h"
#import "DesertCharacterOrientation.h"

@interface DesertHero: NSObject

-(instancetype)init;

-(void)addDesertHeroTo:(SKScene*)scene atPosition:(CGPoint)position;

-(void)runAnimationWithAnimationType:(AnimationType)animationType andWithOrientation:(DesertCharacterOrientation)orientation;

@end

First, please take note of the import statements at the top of the file.  Aside from SpriteKit, our DesertHero class has some dependencies – namely, the enums  AnimationType and DesertCharacterOrientation, which are defined in the header files AnimationType.h and DesertCharacterOrientation.h, respectively.  These two enums will be useful for defining methods for creating and retrieving animations, as will be explained shortly.
The DesertHero class inherits from NSObject, but it will be responsible for managing a SpriteKit node that will display the character animations, as well as other character data relevant to the game (i.e. health, bullets, etc.).  As far as interface methods go, we define a default initializer (to make things simple), and also a method addDesertHeroTo:atPosition, which will add the character to a SpriteKit scene, and runAnimationWithAnimationType:andWithOrientation:, which will be change the animations currently be executed by the character’s sprite node.
Animations for this project include idle, walk, jump, run and shoot, and each of these animations can have a left-facing or right-facing orientation.  To represent these various combinations of orientation and animation type, we define the aforementioned AnimationType and DesertCharacterOrientationenums in separate header files, as show below:

typedef enum{
    IDLE,
    WALK,
    RUN,
    JUMP,
    SHOOT
}AnimationType;

typedef enum{
    LEFT,
    RIGHT,
    SYMMETRICAL
}DesertCharacterOrientation;

In addition to the LEFT and RIGHT cases for the DesertCharacterOrientationenum, I’ve also included a SYMMETRICAL case to account for animations that don’t have a distinct right-facing or left-facing orientation in case we want to include additional animations (e.g. climb, crouch, etc.) later. This will be relevant for future expansions of this project and can be ignored for now.
Now it’s time to implement our DesertHero class.  Let’s create an implementation file for the DesertHero class and add the following import statements at the top:
#import 
#import 

#import "DesertHero.h"
#import "DesertHero+AnimationsGenerator.h"
#import "AnimationType.h"

As you will see, we’ve imported the header file for an Objective-C category(i.e. DesertHero+AnimationsGenerator.h), which we will define shortly. This category provides the methods that we need to generate animations that can be used in the main implementation of the DesertHero class.
In addition, we use Objective-C extensions (indicated via the “()” used in the@interface pre-processor directive)to define what is roughly the equivalent of private variables for our DesertHero.  While extensions are not of primary interest here, it worth discussing them in case the concept is new. Whereas categories are often used to inject new methods into the implementation of a class, extensions are often used to provide additional variables needed to provide additional state information about the class we are implementing:
 


@interface DesertHero()

@property SKSpriteNode* spriteNode;

@property DesertCharacterOrientation currentOrientation;
@property AnimationType currentAnimationType;

@end


@interface DesertHero()

/** Stored references to animations **/

@property SKAction* idleLeftAnimation;
@property SKAction* idleRightAnimation;
@property SKAction* runLeftAnimation;
@property SKAction* runRightAnimation;
@property SKAction* walkLeftAnimation;
@property SKAction* walkRightAnimation;
@property SKAction* jumpLeftAnimation;
@property SKAction* jumpRightAnimation;
@property SKAction* shootLeftAnimation;
@property SKAction* shootRightAnimation;
@property SKAction* climbAnimation;


@end


The extension at the top defines several properties:  spriteNode, which is used to display the character sprite and run the different animations for the character; currentOrientation, which obviously represent the current value of the orientation of the character sprite; and currentAnimationType, which obviously represent the current value of the animationType enum.  The extension at the bottom defines variables that will be used to store the different animations for the DesertHero character.
Now let’s turn to the implementation section of the DesertHero class:
@implementation DesertHero

static NSString* const kDefaultTexture = @"IdleLeft_000";


-(instancetype)init{
    
    self = [super init];
    
    if(self){
        
        SKTexture* texture = [SKTexture textureWithImageNamed:kDefaultTexture];
        
        self.spriteNode = [SKSpriteNode spriteNodeWithTexture:texture];
        
        self.currentAnimationType = IDLE;
        self.currentOrientation = LEFT;
        
        [self configureSpriteNode];
        
        [self configureAnimations];
        
    }
    
    return self;
}


-(void)addDesertHeroTo:(SKScene*)scene atPosition:(CGPoint)position{
    
    [self.spriteNode moveToParent:scene];
    [self.spriteNode setPosition:position];
    
    
}

@end

Above, we have defined a simple default initializer that instantiates a new SKSpriteNode with a default sprite based on a texture whose filename is stored in a constant defined at the top of the implementation file – that is, kDefaultTexture.  The initializer also defines starting values for the currentAnimationType and currentOrientation, and calls some private helper methods that are used to configure the sprite node and animations.  These private helper methods are shown below:
-(void)configureSpriteNode{
    
    NSLog(@"Configuring sprite node...");
    self.spriteNode.anchorPoint = CGPointMake(0.5, 0.5);
    self.spriteNode.xScale *= 0.25;
    self.spriteNode.yScale *= 0.25;
    
    
}



-(void)configureAnimations{
    
    NSLog(@"Configuring animations...");

    
    self.idleLeftAnimation = [self generateIdleAnimation:LEFT];
    self.idleRightAnimation = [self generateIdleAnimation:RIGHT];
    
    self.walkLeftAnimation = [self generateWalkAnimation:LEFT];
    self.walkRightAnimation = [self generateWalkAnimation:RIGHT];
    
    self.runLeftAnimation = [self generateRunAnimation:LEFT];
    self.runRightAnimation = [self generateRunAnimation:RIGHT];
    
    self.jumpLeftAnimation = [self generateJumpAnimation:LEFT];
    self.jumpRightAnimation = [self generateJumpAnimation:RIGHT];
    
    self.shootLeftAnimation = [self generateShootAnimation:LEFT];
    self.shootRightAnimation = [self generateShootAnimation:RIGHT];
    
    
}



The above methods are pretty self-explanatory.  Notice that in our configureAnimations: method, we assign animations to each of the animation properties that we defined in the extension above (we are actually using accessors generated by the compiler via @property keyword, and not assigning directly to the underlying instance variables themselves, which in this case are accessible via variable names beginning with an underline – e.g. as  _idleLeftAnimation_idleRightAnimation, etc.).  The methods used to generate the animations are still somewhat of an enigma – and that’s because we haven’t defined them yet.  Where do they come from? They are from the category that we imported via the #import “DesertHero+AnimationsGenerator.h” statement at the top of our implementation file., which we will get to shortly.
In addition, we implement the interface method that will be responsible for running a specific animation based on the animation type and orientation parameters passed into the function, as shown below:
-(void)runAnimationOnNode:(SKSpriteNode*)node ForAnimationType:(AnimationType)animationType andOrientation:(DesertCharacterOrientation)orientation{
    
    
    SKAction* selectedAnimation = nil;
    
    switch (animationType) {
        case IDLE:
            if(orientation == LEFT){
                selectedAnimation = self.idleLeftAnimation;
            } else if(orientation == RIGHT){
                selectedAnimation = self.idleRightAnimation;
            } else {
                selectedAnimation = nil;
            }
            break;
        case WALK:
            if(orientation == LEFT){
                selectedAnimation = self.walkLeftAnimation;
            } else if(orientation == RIGHT){
                selectedAnimation = self.walkRightAnimation;
            } else {
                selectedAnimation = nil;
            }
            break;
        case RUN:
            if(orientation == LEFT){
                selectedAnimation = self.runLeftAnimation;
            } else if(orientation == RIGHT){
                selectedAnimation = self.runRightAnimation;
            } else {
                selectedAnimation = nil;
            }
            break;
        case SHOOT:
            if(orientation == LEFT){
                selectedAnimation = self.shootLeftAnimation;
            } else if(orientation == RIGHT){
                selectedAnimation = self.shootRightAnimation;
            } else {
                selectedAnimation = nil;
            }
            break;
        case JUMP:
            if(orientation == LEFT){
                selectedAnimation = self.jumpLeftAnimation;
            } else if(orientation == RIGHT){
                selectedAnimation = self.jumpRightAnimation;
            } else {
                selectedAnimation = nil;
            }
            break;
        default:
            break;
    }
    
    NSString* animationKey = [self generateAnimationKeyFor:animationType andFor:orientation];
    
    [node runAction:selectedAnimation withKey:animationKey];
}



The use of if-then statements nested within a switch statement may not be the most elegant way to implement this method, but it suits our purposes here.   This method will ultimately allow us to run any given animation based on the specific orientation and animation type requested via user interaction with UI elements in the AnimationsController.
To make things easier, we can hide the implementation details of the method above and wrap the above method in another method that doesn’t take a node parameter, since we already know that the node being used to execute animations is the spriteNode that is responsible for displaying the character.  To that end, the helper method above will be called in the interface method which we will use in our AnimationsController class to switch between different animations that will be displayed in an embedded SpriteKit view:
-(void)runAnimationWithAnimationType:(AnimationType)animationType andWithOrientation:(DesertCharacterOrientation)orientation{
    
    [self runAnimationOnNode:self.spriteNode ForAnimationType:animationType andOrientation:orientation];
}



Finally, we turn to the the category itself, which will provide the methods needed by our DesertHero class for configuring the different animations for the character.  Below is the public interface for the category, which basically shows method signatures for a series of methods that, given a given orientation, will generate the different animations for the character.
#import "DesertHero.h"
#import "DesertCharacterOrientation.h"
#import "AnimationType.h"


@interface DesertHero (AnimationsGenerator)


-(NSString*)generateAnimationKeyFor:(AnimationType)animationType andFor:(DesertCharacterOrientation)orientation;

-(SKAction*)generateIdleAnimation:(DesertCharacterOrientation)orientation;
-(SKAction*)generateRunAnimation:(DesertCharacterOrientation)orientation;
-(SKAction*)generateJumpAnimation:(DesertCharacterOrientation)orientation;
-(SKAction*)generateShootAnimation:(DesertCharacterOrientation)orientation;
-(SKAction*)generateWalkAnimation:(DesertCharacterOrientation)orientation;


@end

Next, we create the implementation(DesertHere+AnimationsGenerator.m) file for our category. At the top of the implementation, we define static constants for the time between frames for each of the animations:
#import "DesertHero+AnimationsGenerator.h"



@implementation DesertHero (AnimationsGenerator)

const static double kIdleActionDuration = 0.40;
const static double kRunActionDuration = 0.30;
const static double kShootActionDuration = 0.30;
const static double kJumpActionDuration = 0.20;
const static double kWalkActionDuration = 0.30;


@end 

In the implementation file, we will create a helper function which will return a block of signature (SKAciton*(^)(DesertCharacterOrientation)).  This helper function is basically a wrapper for a switch statement.  It takes arrays of SKTextures for left-facing animations, right-facing animations, and symmetric animations.  It then uses the texture arrays to create a block that can be called to generate an SKAction for a given orientation, based on the array of textures passed in as arguments to the wrapper function.
-(SKAction*(^)(DesertCharacterOrientation))getAnimationGeneratorWithLeftOrientationTextures:(NSArray*)leftOrientationTextures andWithRightOrientationTexture:(NSArray*)rightOrientationTextures andWithSymmetricOrientationTextures:(NSArray*)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;
        
        
    };
}




Next we define a method that will generate a string key for an animation based on its animationType and its orientation.  This method is not absolutely necessary for our purposes here and can be skipped if it has no use for your project.  However, sometimes its convenient to not only run animations but also to provide a string key that can be used as an identifier for that animation.  This can come in handy if you have several different animations running on the same node and you wish to selectively remove a specific animation based on a specific event or condition in the game.
-(NSString*)generateAnimationKeyFor:(AnimationType)animationType andFor:(DesertCharacterOrientation)orientation{
    
    NSString* baseStr = [[NSString alloc] init];
    
    switch (animationType) {
        case IDLE:
            baseStr = @"idle";
            break;
        case WALK:
            baseStr = @"walk";
            break;
        case RUN:
            baseStr = @"run";
            break;
        case SHOOT:
            baseStr = @"shoot";
            break;
        case JUMP:
            baseStr = @"jump";
            break;
        default:
            break;
    }
    
    NSString* orientationStr;
    
    switch (orientation) {
        case LEFT:
            orientationStr = @"Left";
            break;
        case RIGHT:
            orientationStr = @"Right";
            break;
        case SYMMETRICAL:
            orientationStr = @"Symmetric";
            break;
        default:
            break;
    }
    
    
    return [baseStr stringByAppendingString:orientationStr];
    
}

If we didn’t define the helper method that generates an animation generator function, we could have alternatively defined a function like the one show below for generating idle animations. This function implementation first creates an SKAction instance based on a given orientation and set of pre-defined “idle” textures. It then passes in this SKAction instance to the SKAction class method, repeatActionForever:, which runs the idle animation on a loop:
-(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;
    
}

However, rather than implementing the animation generator functions the way we’ve shown above, we instead call our helper function getAnimationGeneratorWithLeftOrientationTextures:andWithRightOrientationTexture:andWithSymmetricOrientationTextures:andWithTimePerFrame:, which return a block that is called in the return statement of our animation generator function so as to generate the final, repeating animation:
-(SKAction *)generateRunAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getRunAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:@[
           [SKTexture textureWithImageNamed:@"RunLeft_000"],
           [SKTexture textureWithImageNamed:@"RunLeft_001"],
           [SKTexture textureWithImageNamed:@"RunLeft_002"],
           [SKTexture textureWithImageNamed:@"RunLeft_003"],
           [SKTexture textureWithImageNamed:@"RunLeft_004"],
           [SKTexture textureWithImageNamed:@"RunLeft_005"],
           [SKTexture textureWithImageNamed:@"RunLeft_006"],
           [SKTexture textureWithImageNamed:@"RunLeft_007"],
           [SKTexture textureWithImageNamed:@"RunLeft_008"],
           [SKTexture textureWithImageNamed:@"RunLeft_009"]
           
       ]
      
      andWithRightOrientationTexture:@[
      
           [SKTexture textureWithImageNamed:@"RunRight_000"],
           [SKTexture textureWithImageNamed:@"RunRight_001"],
           [SKTexture textureWithImageNamed:@"RunRight_002"],
           [SKTexture textureWithImageNamed:@"RunRight_003"],
           [SKTexture textureWithImageNamed:@"RunRight_004"],
           [SKTexture textureWithImageNamed:@"RunRight_005"],
           [SKTexture textureWithImageNamed:@"RunRight_006"],
           [SKTexture textureWithImageNamed:@"RunRight_007"],
           [SKTexture textureWithImageNamed:@"RunRight_008"],
           [SKTexture textureWithImageNamed:@"RunRight_009"]
      ] andWithSymmetricOrientationTextures: nil
     andWithTimePerFrame:kRunActionDuration];
    
    return getRunAnimation(orientation);
    
}

-(SKAction *)generateJumpAnimation:(DesertCharacterOrientation)orientation{
    
    SKAction*(^getJumpAnimation)(DesertCharacterOrientation) = [self getAnimationGeneratorWithLeftOrientationTextures:@[
        
    [SKTexture textureWithImageNamed:@"JumpLeft_000"],
    [SKTexture textureWithImageNamed:@"JumpLeft_001"],
    [SKTexture textureWithImageNamed:@"JumpLeft_002"],
    [SKTexture textureWithImageNamed:@"JumpLeft_003"],
    [SKTexture textureWithImageNamed:@"JumpLeft_004"],
    [SKTexture textureWithImageNamed:@"JumpLeft_005"],
    [SKTexture textureWithImageNamed:@"JumpLeft_006"],
    [SKTexture textureWithImageNamed:@"JumpLeft_007"],
    [SKTexture textureWithImageNamed:@"JumpLeft_008"],
    [SKTexture textureWithImageNamed:@"JumpLeft_009"]
                                                                                                            
        ] andWithRightOrientationTexture:@[
       
   [SKTexture textureWithImageNamed:@"JumpRight_000"],
   [SKTexture textureWithImageNamed:@"JumpRight_001"],
   [SKTexture textureWithImageNamed:@"JumpRight_002"],
   [SKTexture textureWithImageNamed:@"JumpRight_003"],
   [SKTexture textureWithImageNamed:@"JumpRight_004"],
   [SKTexture textureWithImageNamed:@"JumpRight_005"],
   [SKTexture textureWithImageNamed:@"JumpRight_006"],
   [SKTexture textureWithImageNamed:@"JumpRight_007"],
   [SKTexture textureWithImageNamed:@"JumpRight_008"],
   [SKTexture textureWithImageNamed:@"JumpRight_009"]
       ] andWithSymmetricOrientationTextures:nil andWithTimePerFrame:kJumpActionDuration];
    
    return getJumpAnimation(orientation);
}



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


    
    ]
    andWithRightOrientationTexture:
  @[
    [SKTexture textureWithImageNamed:@"ShootRight_000"],
    [SKTexture textureWithImageNamed:@"ShootRight_001"],
    [SKTexture textureWithImageNamed:@"ShootRight_002"],
    [SKTexture textureWithImageNamed:@"ShootRight_003"],
    [SKTexture textureWithImageNamed:@"ShootRight_004"],
    [SKTexture textureWithImageNamed:@"ShootRight_005"],
    [SKTexture textureWithImageNamed:@"ShootRight_006"],
    [SKTexture textureWithImageNamed:@"ShootRight_007"],
    [SKTexture textureWithImageNamed:@"ShootRight_008"],
    [SKTexture textureWithImageNamed:@"ShootRight_009"],


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



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


    ]
   andWithRightOrientationTexture:
  @[
    [SKTexture textureWithImageNamed:@"WalkRight_000"],
    [SKTexture textureWithImageNamed:@"WalkRight_001"],
    [SKTexture textureWithImageNamed:@"WalkRight_002"],
    [SKTexture textureWithImageNamed:@"WalkRight_003"],
    [SKTexture textureWithImageNamed:@"WalkRight_004"],
    [SKTexture textureWithImageNamed:@"WalkRight_005"],
    [SKTexture textureWithImageNamed:@"WalkRight_006"],
    [SKTexture textureWithImageNamed:@"WalkRight_007"],
    [SKTexture textureWithImageNamed:@"WalkRight_008"],
    [SKTexture textureWithImageNamed:@"WalkRight_009"],

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



Now we will create the interface for the SKScene that will be presented by the SKView embedded in our AnimationsController.   This interface will have a single method  runAnimationWithAnimationType:andWithOrientation:, which can be called to activate different character animations from our AnimationsController.  It will basically act as a wrapper for the DesertHeroclass instance method runAnimationWithAnimationType:andWithOrientation:.   The implementation file for the animation scene will also configure the background and add the DesertHero character to the scene in the didMoveToView function.
AnimationScene.h
@import SpriteKit;
#import "AnimationType.h"
#import "DesertCharacterOrientation.h"

@interface AnimationScene: SKScene

-(void)runAnimationWithAnimationType:(AnimationType)animationType andWithOrientation:(DesertCharacterOrientation)orientation;


@end


AnimationScene.m 

#import "AnimationScene.h"
#import "DesertHero.h"

@interface AnimationScene()

@property DesertHero* hero;

@end



@implementation AnimationScene


-(void)didMoveToView:(SKView *)view{
    
    self.anchorPoint = CGPointMake(0.5, 0.5);
    
    [self configureBackground];
    
    self.hero = [[DesertHero alloc] init];
    
    [self.hero addDesertHeroTo:self atPosition:CGPointZero];
    
}

-(void)configureBackground{
    

    SKSpriteNode* backgroundSprite = [SKSpriteNode spriteNodeWithImageNamed:@"desert_background"];
    
    backgroundSprite.anchorPoint = CGPointMake(0.5, 0.5);
    
    backgroundSprite.zPosition = -10;
    
    [backgroundSprite setScale:0.5];
    
    [backgroundSprite moveToParent:self];
}

-(void)runAnimationWithAnimationType:(AnimationType)animationType andWithOrientation:(DesertCharacterOrientation)orientation{
    
    [self.hero runAnimationWithAnimationType:animationType andWithOrientation:orientation];

    
}


@end


Now we are ready to create a header file for the AnimationsController, which will also act as the backing class for the view controller that we configure in our Main.storyboard file.   The AnimationsController is configured in the Main.storyboard, a screenshot for which is shown below.  Basically, it has segmented control for toggling the orientation of the character at the top, an embedded SKView (which will display the DesertHero character in different animation states), and a set of buttons embedded in a UIStackViewthat can be used to select from different animations that can be displayed in the SKView:
AnimationsController.h
@import UIKit;

@interface AnimationsController: UIViewController


@end


Main.storyboard
animationsController_storyboard
AnimationsController.m
#import 
#import 
#import "AnimationsController.h"
#import "AnimationScene.h"
#import "AnimationType.h"
#import "DesertCharacterOrientation.h"


@interface AnimationsController()

@property (weak, nonatomic) IBOutlet SKView *skView;

@property AnimationScene* animationScene;
@property DesertCharacterOrientation currentOrientation;
@property AnimationType currentAnimationType;

@property (weak, nonatomic) IBOutlet UISegmentedControl *orientationSegmentedControl;



@end

As shown above, the implementation file for our AnimationsControllerdefines properties for the animationScene, the SKScene being displayed by the SKView view, and also for currentOrientation and currentAnimationType, which are used to toggle between different animation states.  Below is the viewDidLoad function for the AnimationsController:
@implementation AnimationsController

-(void)viewDidLoad{
    
    CGSize size = self.skView.bounds.size;
    
    self.animationScene = [[AnimationScene alloc] initWithSize:size];
    
    [self.skView presentScene:self.animationScene];
}

We also define a helper method runAnimation, that gets called each time the user toggles to a different animation state, either as a result of tapping the segmented control for character orientation or by tapping on a button to a select a different animation type:
-(void)runAnimation{
    
    [self.animationScene runAnimationWithAnimationType:self.currentAnimationType andWithOrientation:self.currentOrientation];
}

Below are the IBActions connected to the different UI elements in the AnimationsController. These IBActions change the value stored in currentOrientation and currentAnimation properties defined above, and they also call the runAnimation method so as to display the new animation for the character.
- (IBAction)changeOrientation:(UISegmentedControl *)sender {
    
    self.currentOrientation = (DesertCharacterOrientation)self.orientationSegmentedControl.selectedSegmentIndex;
    
    [self runAnimation];

}



- (IBAction)showIdleAnimation:(UIButton *)sender {
    
    self.currentAnimationType = IDLE;

    [self runAnimation];
}

- (IBAction)showWalkAnimation:(UIButton *)sender {
    
    self.currentAnimationType = WALK;

    [self runAnimation];

}


- (IBAction)showRunAnimation:(UIButton *)sender {
    
    self.currentAnimationType = RUN;

    [self runAnimation];

}

- (IBAction)showJumpAnimation:(UIButton *)sender {
    
    self.currentAnimationType = JUMP;

    [self runAnimation];

}
- (IBAction)showShootAnimation:(UIButton *)sender {
    
    self.currentAnimationType = SHOOT;

    [self runAnimation];

}


@end

No comments:

Post a Comment