Wednesday, August 8, 2018

Objective-C Protocols (Part 1): Applications for iOS Gaming

*For a more holistic understanding of this mini-game with specific emphasis on the application of iOS protocols, please go to main page for this tutorial here.   Otherwise, if you are interested in more specific details about the implementation of the different classes in this game, feel free to continue reading. In this tutorial, we will develop a small demo game to illustrate the utility of Objective-C protocols in iOS game design. In the game, a collection of letters is spawned, and the letters appear and disappear at regular intervals.  When the letters appear, they are scattered at random positions on the screen.  When they disappear, they hide behind clouds in the night sky.  The user is given a target word displayed at the bottom of the screen, and the goal of the game is to spell the target word by tapping on the letters in the order in which they appear in the word.  Here is a video screen shot of the game:

  Video.  Galactic Letters 



 You can clone the project or download the source code for the final project at this Github repository.  Otherwise, you can just follow along and hopefully everything will make sense. First, we will create a helper class RandomPointGenerator, which will generate random points on screen so that we can scatter our letters across the screen at regular intervals and make it more challenging for the user to spell the target word.  The header and implementation files for this class are show below:

RandomPointGenerator.h
 

#import 

@interface RandomPointGenerator : NSObject

-(instancetype)init;
-(instancetype)initForDebugSpawnArea;

-(CGPoint)getRandomOnScreenCoordinate;
-(NSArray*)getArrayOfOnScreenPoints:(NSInteger)numberOfPoints;

@end

RandomPointGenerator.m
#import 
#import 
#import "RandomPointGenerator.h"
#import "ScreenConstants.h"
#import "Constants.h"

@interface RandomPointGenerator()

@property GKMersenneTwisterRandomSource* randomXSource;
@property GKMersenneTwisterRandomSource* randomYSource;

@property GKRandomDistribution* randomXDist;
@property GKRandomDistribution* randomYDist;

@end



@implementation RandomPointGenerator


-(NSArray*)getArrayOfOnScreenPoints:(NSInteger)numberOfPoints{
    
    NSMutableArray* pointArray = [NSMutableArray arrayWithCapacity:numberOfPoints];
    
    for (int i = 0; i < numberOfPoints; i++) {
        
        CGPoint randomPoint = [self getRandomOnScreenCoordinate];
        
        NSValue* randomPointVal = [NSValue valueWithCGPoint:randomPoint];
        
        [pointArray addObject:randomPointVal];
        
    }
    
    return [NSArray arrayWithArray:pointArray];
}

-(CGPoint)getRandomOnScreenCoordinate{
    
    NSInteger xCoord = [self getRandomOnScreenXCoord];
    NSInteger yCoord = [self getRandomOnScreenYCoord];
    
    return CGPointMake(xCoord, yCoord);
}

-(NSInteger)getRandomOnScreenXCoord{
    return [self.randomXDist nextInt];
}

-(NSInteger)getRandomOnScreenYCoord{
    return [self.randomYDist nextInt];
}


-(instancetype)initForDebugSpawnArea{
    
    self = [super init];
    
    if(self){
        _randomXSource = [[GKMersenneTwisterRandomSource alloc] init];
        _randomYSource = [[GKMersenneTwisterRandomSource alloc] init];
        
        int lowestValX = kDebugSpawnArea_LowerX;
        int highestValX = kDebugSpawnArea_UpperX;
        
        int lowestValY = kDebugSpawnArea_LowerY;
        int highestValY = kDebugSpawnArea_UpperY;
        
        self.randomXDist = [[GKRandomDistribution alloc] initWithRandomSource:_randomXSource lowestValue:lowestValX highestValue:highestValX];
        
        self.randomYDist = [[GKRandomDistribution alloc] initWithRandomSource:_randomYSource lowestValue:lowestValY highestValue:highestValY];
        
    }
    
    return self;
}

-(instancetype)init{
    
    self = [super init];
    
    if(self){
        
        _randomXSource = [[GKMersenneTwisterRandomSource alloc] init];
        _randomYSource = [[GKMersenneTwisterRandomSource alloc] init];

        int lowestValX = (int)kLeftXBoundary;
        int highestValX = (int)kRightXBoundary;
        
        int lowestValY = (int)kLowerYBoundary;
        int highestValY = (int)kUpperYBoundary;
        
        self.randomXDist = [[GKRandomDistribution alloc] initWithRandomSource:_randomXSource lowestValue:lowestValX highestValue:highestValX];
        
        self.randomYDist = [[GKRandomDistribution alloc] initWithRandomSource:_randomYSource lowestValue:lowestValY highestValue:highestValY];

    }
    
    return self;
}

@end

The interface for this class provides two initializers init and initForDebugSpawnArea,  as well as two methods that we will use to position our game objects- that is, getRandomOnScreenCoordinate, which returns a random point on screen, and getArrayOfOnScreenPoints, which returns an array of random on-screen points wrapped in NSValue objects. In the implementation file, you can see that we define several properties: randomXSource and randomYSource, which are used to initialize GKRandomDistribution objects, as required by GameplayKit. The other properties include randomXDist and randomYDist, which basically correspond to the Gaussian distributions that will be used to generate random on-screen X-coordinates and Y-coordinates, respectively. Please note that the initializer for the GKRandomDistribution object also requires a lower and an upper bounds, which, in this case, are provided by global constants that we define in a ScreenConstants.h file, shown below:
 

#define kScreenWidth UIScreen.mainScreen.bounds.size.width;
#define kScreenHeight UIScreen.mainScreen.bounds.size.height;
#define kLeftXBoundary -UIScreen.mainScreen.bounds.size.width/2.00+50;
#define kRightXBoundary UIScreen.mainScreen.bounds.size.width/2.00-50;
#define kUpperYBoundary UIScreen.mainScreen.bounds.size.height/2.00-50;
#define kLowerYBoundary -UIScreen.mainScreen.bounds.size.height/2.00+50;

The implementation file for the RandomPointGenerator defines two helper methods getRandomOnScreenXCoord and getRandomOnScreenYCoord, which are used to get a random X-coordinate and a random Y-coordinate respectively:
-(NSInteger)getRandomOnScreenXCoord{
    return [self.randomXDist nextInt];
}

-(NSInteger)getRandomOnScreenYCoord{
    return [self.randomYDist nextInt];
}


These helper methods are then called in additional helper methods to generate random points on-screen, as well as an array of random points on-screen:
-(NSArray*)getArrayOfOnScreenPoints:(NSInteger)numberOfPoints{
    
    NSMutableArray* pointArray = [NSMutableArray arrayWithCapacity:numberOfPoints];
    
    for (int i = 0; i < numberOfPoints; i++) {
        
        CGPoint randomPoint = [self getRandomOnScreenCoordinate];
        
        NSValue* randomPointVal = [NSValue valueWithCGPoint:randomPoint];
        
        [pointArray addObject:randomPointVal];
        
    }
    
    return [NSArray arrayWithArray:pointArray];
}

-(CGPoint)getRandomOnScreenCoordinate{
    
    NSInteger xCoord = [self getRandomOnScreenXCoord];
    NSInteger yCoord = [self getRandomOnScreenYCoord];
    
    return CGPointMake(xCoord, yCoord);
}

This is relatively straightforward, and it will come in handy when we are implementing our LetterManager class.  Before we embark upon the journey of making our LetterManager class, we need to create a Letter class that will manage the node for displaying a letter sprite and for storing other state information about the letter, such as its its recovery state (i.e. whether or not the letter is in a state of temporary invulnerability due to recent contact with an enemy object), its health (if you intend to create a version of the game where the letter can be damaged via contact with other game objects),  or its index within a given word. You can get the sprites for these letters for free by going to Kenney.nl.  The clouds used for creating the night sky background are also from Kenney as well.  It a great site for public domain game assets that can be used in not just games but other projects as well: [gallery ids="335,336,337,338,339,340,341" type="rectangular"] Below is interface for our Letter class: Letter.h
#import 
#import 
#import "LetterDelegate.h"

/** The letter object manages an SKSpriteNode which provides the visual representation for the letter in question; the letter object can also contain additional data, such as the number of points associated with a letter **/

@interface Letter: NSObject

-(instancetype)initWithLetter:(char)letter;
-(instancetype)initWithLetter:(char)letter andWithWordIndex:(NSUInteger)wordIndex;
-(instancetype)initWithLetter:(char)letter andWithStartingHealth: (int)startingHealth andWithWordIndex:(NSUInteger)wordIndex;

-(void)addLetterTo:(SKScene*)scene atPosition:(CGPoint)position;
-(void)setLetterPositionTo:(CGPoint)position;
-(void)removeLetter;

@property (readonly)NSString* identifier;
@property (readonly)int pointValue;

@property (weak) id delegate;

-(void)update:(NSTimeInterval)currentTime;
-(void)takeDamage:(int)damageAmount;

+(int)pointsForLetter:(char)wordChar;

@end

You will notice that the interface includes a class method pointsForLetter, which allows us to get the point value for a specific letter.  If you scroll back up to the pictures of the letter sprites, you will notice a small number in the lower right hand corner, which corresponds to the point value of the letter.  By extension, a given word has a point value equal to the sum of the individual point values of each letter, so that when user successfully spell different words, they earn different amounts of points.  This class method is implemented in the implementation section of the Letter class, as show below: Letter.m
+(int)pointsForLetter:(char)wordChar{
    
    switch (wordChar) {
        case 'A':
            return 1;
        case 'B':
            return 3;
        case 'C':
            return 3;
        case 'D':
            return 2;
        case 'E':
            return 1;
        case 'F':
            return 4;
        case 'G':
            return 2;
        case 'H':
            return 4;
        case 'I':
            return 1;
        case 'J':
            return 5;
        case 'K':
            return 8;
        case 'L':
            return 1;
        case 'M':
            return 3;
        case 'N':
            return 1;
        case 'O':
            return 1;
        case 'P':
            return 3;
        case 'Q':
            return 1;
        case 'R':
            return 1;
        case 'S':
            return 1;
        case 'T':
            return 1;
        case 'U':
            return 1;
        case 'V':
            return 4;
        case 'W':
            return 4;
        case 'X':
            return 8;
        case 'Y':
            return 4;
        case 'Z':
            return 10;
        default:
            return 1;
    }
}

For this method, we pass in a character as an argument. All that's required is that we switch on the character and return the appropriate point value accordingly. Here, it's good to note that Objective-C is a superset of C, so the libraries and data types used in a C/C++ project can also be conveniently used in an Objective-C project. In the public interface for the Letter class, you will notice that we defined a readonly property whereby we can represent the point value of a given letter. Letter.h
 

@property (readonly)int pointValue;

In our implementation of the letter class, we call the class method pointsForLetter, to implement this readonly property. Note the letter class also has private property (i.e. a property defined in an extension) of type char that is used to represent the character corresponding to the Letter object in question. This letter is passed in as an argument to the function pointsForLetter, to get the pointValue for the Letter object in question. Letter.m
-(int)pointValue{
    return [Letter pointsForLetter:self.letterChar];
}

Before we get too far ahead of ourselves, let's back-track a little bit and go back to the public interface for the Letter class. There we noticed that there were several initializers that could be invoked to instantiate a letter object. The corresponding implementations for these initializers are shown in the implementation file for the letter below: Letter.m
#import 
#import 
#import "Letter.h"
#import "ContactBitMasks.h"
#import "Constants.h"

@interface Letter()

@property SKSpriteNode* sprite;
@property int health;
@property NSUInteger wordIndex;
@property char letterChar;

@property BOOL isRecovering;
@property NSTimeInterval recoveryInterval;
@property NSTimeInterval frameCount;
@property NSTimeInterval lastUpdateTime;

@end


@implementation Letter

const static double recoveryTime = 0.60;


-(instancetype)initWithLetter:(char)letter{
    
    self = [self initWithLetter:letter andWithWordIndex:0];
    
    return self;
}

-(instancetype)initWithLetter:(char)letter andWithWordIndex:(NSUInteger)wordIndex{
    
    self = [self initWithLetter:letter andWithStartingHealth:3 andWithWordIndex:wordIndex];
    
    return self;
}

-(instancetype)initWithLetter:(char)letter andWithStartingHealth: (int)startingHealth andWithWordIndex:(NSUInteger)wordIndex{

    self = [super init];
    
    if(self){
        
        letter = toupper(letter);
        
        self.letterChar = letter;
        self.health = startingHealth;
        self.wordIndex = wordIndex;
        self.isRecovering = NO;
        self.frameCount = 0.00;
        self.recoveryInterval = recoveryTime;
        
        NSString* spriteName = [NSString stringWithFormat:@"letter_%c",letter];

        SKTexture* letterTexture = [SKTexture textureWithImageNamed:spriteName];
        
        self.sprite = [SKSpriteNode spriteNodeWithTexture:letterTexture];
        
        
        [self configureSpriteWithNodeName:spriteName];
        
        [self configurePhysicsProperties:letterTexture];
        
    }
    
    return self;
}

These initializers provide initial values not only for properties defined in the public interface but also for those in the extension defined in the implementation.  If you are not familiar with the concept of an extension in Objective-C, well, it basically is a way of defining private properties that can be accessed in the implementation of a class.  In our implementation file for the Letter class, you can see that we have define additional properties in an extension, as shown below: Letter.m
 

@interface Letter()

@property SKSpriteNode* sprite;
@property int health;
@property NSUInteger wordIndex;
@property char letterChar;

@property BOOL isRecovering;
@property NSTimeInterval recoveryInterval;
@property NSTimeInterval frameCount;
@property NSTimeInterval lastUpdateTime;

@end

This extension includes several properties whose usage in the implementation of the Letter class can be hidden from the user (i.e. the programmer). The sprite will hold the SKSpriteNode that is displays the sprite for the letter; health is of integer type and will be used to track the health of the letter (which will be relevant for versions of the game where the letter can take damage via collisions); wordIndex, of type NSUInteger, represents the position of the letter in a word; letterChar, of type char, represents the actual character this Letter object represents; isRecovering, a Boolean flag, represents the damage state of the letter, which is used to give a letter temporary invulnerability after it's been damaged (this way, the letter does suffer excessive damage and immediately die upon a simple contact with an enemy object). The variables recoveryInterval, frameCount, and lastUpdate time, all of which are of type NSTimeInterval, are all used to implement a timer functionality whereby the letter appears and disappears behind on-screen clouds at regular intervals. In the implementation section, we've also defined a constant of type double for the time interval at which the letter changes between appearing and hiding states. Letter.m
 

const static double recoveryTime = 0.60;

This constant can just as easily be defined in a separate constants file. All of the properties defined in both the extension and the interface have to be initialized, and of all the initializers shown, one would be considered the designated initializer (the main initializer on which all the other ones depend)while the others would be considered convenience initializers (as they all call or delegate to the designated initializer): Letter.m
 

-(instancetype)initWithLetter:(char)letter andWithStartingHealth: (int)startingHealth andWithWordIndex:(NSUInteger)wordIndex{

    self = [super init];
    
    if(self){
        
        letter = toupper(letter);
        
        self.letterChar = letter;
        self.health = startingHealth;
        self.wordIndex = wordIndex;
        self.isRecovering = NO;
        self.frameCount = 0.00;
        self.recoveryInterval = recoveryTime;
        
        NSString* spriteName = [NSString stringWithFormat:@"letter_%c",letter];

        SKTexture* letterTexture = [SKTexture textureWithImageNamed:spriteName];
        
        self.sprite = [SKSpriteNode spriteNodeWithTexture:letterTexture];
        
        
        [self configureSpriteWithNodeName:spriteName];
        
        [self configurePhysicsProperties:letterTexture];
        
    }
    
    return self;
}


This designated initializer calls the C-function toupper() to ensure that self.letter is upper-cased. It uses the self.letter property to construct the string that is used for the name of the texture that is used in turn to get the sprite for the letter character in question. In addition, this designated initializer calls upon two helper methods, configureSpriteWithNodeName and configurePhysicsProperties, which perform some additional configuration work for the sprite node and the physics body associated with the sprite node. In the implementation file, we also implement the function addLetterTo:atPosition, which adds the sprite for a letter to the GameScene.  Note this function sets the zPosition of the letter sprite using kZPositionLetter, which is a constant defined in our Constants.h file and therefore available for use here.  We also implement setLetterPositionTo, which sets the position of the sprite node for the letter, and update, which toggles the isRecovering variable at regular intervals. Letter.m
 


-(void)addLetterTo:(SKScene*)scene atPosition:(CGPoint)position{
    
    [self.sprite moveToParent:scene];
    [self.sprite setZPosition:kZPositionLetter];
    [self.sprite setPosition:position];
}

-(void)setLetterPositionTo:(CGPoint)position{
    
    SKAction* moveAction = [SKAction moveTo:position duration:0.5];
    [self.sprite runAction:moveAction];
    
}

-(void)update:(NSTimeInterval)currentTime{
    
    if(currentTime == 0){
        self.frameCount = currentTime;
    }
    
    self.frameCount += currentTime - self.lastUpdateTime;
    
    if(self.frameCount > self.recoveryInterval){
        
        self.isRecovering = !self.isRecovering;
        
        self.frameCount = 0.00;
    }
    
    self.lastUpdateTime = currentTime;
    
}



The implementation section also includes implementations of removeLetter, which removes the Letter object's sprite node from its parent and also calls the Letter object's delegate method didDestroyLetter, which informs the Letter's delegate that the letter has been removed from its node hierarchy (We will get to the delegate shortly!).  It also includes implementation of runDamageAnimation, which runs a fadeAction on the Letter's sprite node when it has been damaged via contact with an enemy, and die, which like the removeLetter method, removes the Letter's sprite node from the node hierarchy and calls the delegate method didDestroyLetter.  The die method can in fact be refactored so as to call the removeLetter method, if you want to make your code less repetitive. Letter.m
 


-(void)removeLetter{
    [self.sprite removeFromParent];
    [self.delegate didDestroyLetter:self];

}

-(void)runDamageAnimation{
    NSLog(@"Letter has been damaged...");
    
    SKAction* fadeAction = [SKAction fadeAlphaBy:0.20 duration:0.10];
    
    [self.sprite runAction:fadeAction];
}



-(void)die{
    NSLog(@"Letter is dead...");

    [self.sprite removeFromParent];
    [self.delegate didDestroyLetter:self];

}

Finally, for versions of the game where the letter can take damage such that it's health value increases or decreases, we implement a takeDamage function, which first checks if the Letter is in a recovering state (in which case we return from the method immediately), then decreases the health value of the letter, and then runs an animation based on the health value of the letter by switching on the value of self.health. Letter.m
-(void)takeDamage:(int)damageAmount{
    
    if(self.isRecovering){
        NSLog(@"Letter is recovering...");
        return;
    }
    
    NSLog(@"Letter is taking damage...");
    
    self.health -= damageAmount;
    
    
    switch (self.health) {
        case 3:
            [self runDamageAnimation];
            break;
        case 2:
            [self runDamageAnimation];
            break;
        case 1:
            [self runDamageAnimation];
            break;
        case 0:
            [self die];
            
            break;
        default:
            break;
    }
    
   
    
}


If you scroll back up to the interface for the Letter class, you should notice that we define a property for a delegate that conforms to the LetterDelegate protocol.   This protocol defines methods that are called when a letter is damaged or destroyed, and it will become important in versions of this game where the letter is subject to damage, but you can ignore it for now, since it's not essential to our purposes here. You will have noticed that the above methods have often called delegate methods as well.  The LetterDelegate methods are defined below: LetterDelegate.h
#import 

@class Letter;

@protocol LetterDelegate 


@optional

-(void)didDamageLetter:(Letter*)letter;

@required

-(void)didDestroyLetter:(Letter*)letter;

@end


That's good enough for our Letter class.  We still have a lot more work to do, including implementing a LetterManager class, which will be responsible for managing the spawning and update of a collection of letters displayed on screen, so let's continue to the next section...

No comments:

Post a Comment