Wednesday, August 8, 2018

Objective-C Protocols (Part 2): 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 the previous section, we defined a Letter class that would be responsible for managing the SKSpriteNode for displaying the sprite in our game as well as for storing state information relevant to a given letter object, such as health, wordIndex, and whether or not the the letter is in a state of recovery after contacting an enemy object. We continue by defining a LetterManager class, whose header and implementation files are provided below.   The LetterManager spawns the letters for a given target word, adding them to the scene and randomizing their positions at regular update intervals. It also handles some of the contact logic between letters and other game objects. The letter manager should not be confused with the WordManager (another class that we will define later), which manages the target word (i.e. the word that the user aims to spell correctly) and word in progress (i.e. the word currently being spelled by the user). LetterManager.h
@interface LetterManager: NSObject 

@property NSString* targetWord;


-(instancetype)initWithSpawnPoints:(NSArray*)spawnPoints andWithTargetWord:(NSString*)targetWord;

-(void)update:(NSTimeInterval)currentTime;

-(void)addLettersTo:(SKScene*)scene;
-(void)clearLetters;

-(void)handleContactForLetterWith:(NSString*)letterIdentifier andWithContactedObjectName:(NSString*)contactedObjectName;

@end

LetterManager.m
#import 
#import 
#import 
#import "RandomPointGenerator.h"
#import "LetterManager.h"
#import "Letter.h"
#import "LetterDelegate.h"



@interface LetterManager() 

@property NSMutableArray* letters;
@property NSArray* spawnPoints;

@property NSTimeInterval repositionInterval;
@property NSTimeInterval frameCount;
@property NSTimeInterval lastUpdateTime;

@property BOOL lettersAreHidden;
@property RandomPointGenerator* pointGenerator;

@end


@implementation LetterManager



-(instancetype)initWithSpawnPoints:(NSArray*)spawnPoints andWithTargetWord:(NSString*)targetWord{

    self = [super init];
    
    if(self){
        self.spawnPoints = spawnPoints;
        self.targetWord = targetWord;
        self.letters = [[NSMutableArray alloc] initWithCapacity:self.targetWord.length];
        self.lettersAreHidden = YES;
        self.pointGenerator = [[RandomPointGenerator alloc] init];
        
        self.frameCount = 0.00;
        self.repositionInterval = 4.00;
        self.lastUpdateTime = 0.00;
    }
    
    return self;

}


-(void)addLettersTo:(SKScene*)scene{
    
    for (int charIndex = 0; charIndex < self.targetWord.length; charIndex++) {                  char wordChar = [self.targetWord characterAtIndex:charIndex];                  Letter* newLetter = [[Letter alloc] initWithLetter:wordChar andWithWordIndex:charIndex];                  newLetter.delegate = self;                  CGPoint randomPos = [self getRandomSpawnPointPosition];                  [newLetter addLetterTo:scene atPosition:randomPos];                  [self.letters addObject:newLetter];              } } -(void)update:(NSTimeInterval)currentTime{                    if(self.frameCount == currentTime){         self.frameCount = 0;     }               self.frameCount += currentTime - self.lastUpdateTime;          //Update all of the letters     for (Letter*letter in self.letters) {                  [letter update:currentTime];     }     if(self.frameCount > self.repositionInterval){
        
        
        if(self.lettersAreHidden){
            
            [self revealLetters];
            
        } else {
        
            [self hideLetters];

        }
        
        self.frameCount = 0;
    }
    
    self.lastUpdateTime = currentTime;
}



-(void)revealLetters{
    
    for (Letter*letter in self.letters) {
        CGPoint randomOnScreenPos = [self getRandomOnScreenPosition];
        [letter setLetterPositionTo:randomOnScreenPos];
    }
    
    self.lettersAreHidden = NO;

}

-(void)hideLetters{
    for (Letter*letter in self.letters) {
        
        CGPoint randomPos = [self getRandomSpawnPointPosition];
        [letter setLetterPositionTo: randomPos];
        
    
    }
    self.lettersAreHidden = YES;
}

-(CGPoint)getRandomOnScreenPosition{
    
    return [self.pointGenerator getRandomOnScreenCoordinate];
}

-(CGPoint)getRandomSpawnPointPosition{
    
    NSUInteger randomIndex = arc4random_uniform((UInt32)self.spawnPoints.count);
    
    NSValue* positionVal = [self.spawnPoints objectAtIndex:randomIndex];
    
    return positionVal.CGPointValue;
}


/** The contacted letter can take different amounts of damage based on the enemy that contacts it **/

-(void)handleContactForLetterWith:(NSString*)letterIdentifier andWithContactedObjectName:(NSString*)contactedObjectName{
    
    NSLog(@"The letter identifiers is: %@",letterIdentifier);
    
    NSPredicate* predicate = [NSPredicate predicateWithFormat:@"identifier LIKE %@",letterIdentifier];
    
   Letter* contactedLetter = [[self.letters filteredArrayUsingPredicate:predicate] firstObject];
    
    NSString* contactedLetterInfo = [contactedLetter debugDescription];
    
    NSLog(@"Contacted letter info: %@",contactedLetterInfo);
    
    [contactedLetter takeDamage:1];
    

}

-(void)clearLetters{
    
  
    self.letters = nil;
    
    self.letters = [[NSMutableArray alloc] init];

}

/** Conformance to LetterDelegate protocol **/

-(void)didDestroyLetter:(Letter *)letter{
    
    [self.letters removeObject:letter];
}




@end

It will be noted that the implementation file for the LetterManager requires the RandomPointGenerator, which we defined in the previous section, as a dependency, and also defines a property for a pointGenerator in an extension.  The pointGenerator instance method getRandomOnScreenCoordinate is called in the function getRandomOnScreenPosition, which in turn is used to generate random on-screen points that are used to reposition the letters at random positions after the letters have been hiding for fixed interval. During initialization of the LetterManager class, an array of NSValue objects containing a set of spawn points is used to initialize the spawnPoints property.  This is basically an array of points representing the positions of the all the clouds on the screen, whose positions themselves are determined randomly using a RandomPointGenerator in the GameScene class.  This is why the LetterManager class has another instance method getRandomSpawnPointPosition which randomly selects a spawn point from the array of spawn points in self.spawnPoints in order to cause the on-screen letters to hide behind a random on-screen cloud after appearing on-screen for a fixed period of time.  Hence, the revealLetters and hideLetters instance methods are called alternately in the update function, causing letters to appear at random on-screen positions and then hide randomly from one among any one of a number of the spawn positions represented by the on-screen background clouds. **The didDestroyLetter method is implemented in order to make the LetterManager class conform to the LetterDelegate protocol, which we defined in the previous section.  Each letter that is spawned by the LetterManager has its delegate set to the LetterManager in order that the LetterManager can perform any tasks associated with a letter being destroyed.  In this version of the game, however, we won't be concerned with letters being destroyed or damaged in any way, so this can basically be ignored.  Likewise, the instance method handleContactForLetterWith can also be ignored,  since it is only relevant for games where collision logic is implemented.  But in case you were curious, if you will recall, the interface for a Letter has an identifier property, whose implementation involves retrieving the name of sprite node managed by the letter.  This identifier is retrieved in a collision handler (i.e. specifically the SKPhysicsContactDelegate method didBeginContact) and used to identify the specific letter involved in a collision so that the LetterManager can process any changes in state accordingly (e.g. decreased health, temporary invulnerabilitiy, etc.).  For this reason,  we defined a class method in in the Letter class getLetterCharacterFromPhysicsBody, which is utility method for accessing the node name of the node with which a physics body is associated.  Since we only have access to physics bodies, this method enables us to gain quick access to the node name, which is a unique identifier that can be used to identify the specific letter involved in the collision.   If you are interested, an alternative implementation involves adding a category to the SKPhysicsBody class - that is, we define a category that provides additional functionality for an SKPhysicsBody and then import the header file for this category in our main GameScene file. Now, going back to our LetterManger class, let's take a look at another important method addLettersTo, shown below:
 


-(void)addLettersTo:(SKScene*)scene{
    
    for (int charIndex = 0; charIndex < self.targetWord.length; charIndex++) {
        
        char wordChar = [self.targetWord characterAtIndex:charIndex];
        
        Letter* newLetter = [[Letter alloc] initWithLetter:wordChar andWithWordIndex:charIndex];
        
        newLetter.delegate = self;
        
        CGPoint randomPos = [self getRandomSpawnPointPosition];
        
        [newLetter addLetterTo:scene atPosition:randomPos];
        
        [self.letters addObject:newLetter];
        
    }
}

  In order to understand this method, we have to remember that, in addition to an array of NSValues representing the spawn points where letters can hide at regular intervals, we provided a variable targetWord of type NSString to our initializer for the LetterManager.  This is the word that must be spelled by the user, and in our addLettersTo function we use a for-loop to iterate through all of the characters in the target word, using the index for the position of the character as well as the character itself to instantiate a Letter object, whose delegate we set to the LetterManager class. This letter also is added to the node hierarchy of the scene as well as the self.letter array, which is a property defined in an extension for the LetterManger class and which used to keep track of all the Letter whose sprites are being currently displayed on screen. Now that we have our LetterManager class defined, let's move on to the heart of this tutorial, the WordManager, for which we will define a data source and delegate protocols, and which will be responsible for handling the main logic of the game. Or if you are not comfortable with the material presented here, feel free to return to the previous section for more review.

No comments:

Post a Comment