Chapter 13: Who’s Keeping Score?

In the last chapter, we got the targets and the target manager working, and added code to detect whether or not the player touched a target. We already have the code in place that knows if a touch is a hit or not, and can score accordingly, so all we really need to do now is add a global score counter and text string object.

There’s one other thing that’s bothering me, though. When a game is over, it flies right into the next game as I’m still trying to touch targets from the last game. We need some sort of delay between games so we have time to stop touching before it starts another game. After all, we need time to look at our score before we reset it by starting another game.

Let’s start by making a few modification to the TouchTargetsViewController.h file.

#import <UIKit/UIKit.h>

#import <OpenGLES/EAGL.h>

#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>

#import "EDTextStringManager.h"
#import "EDTargetManager.h"

@interface TouchTargetsViewController : UIViewController {
@private
    EAGLContext *context;
   
    BOOL animating;
    NSInteger animationFrameInterval;
    CADisplayLink *displayLink;
   
    EDTextStringManager *myTextStringManager;
   
    EDTextString *myStatusMessage;
    EDTextString *myScoreMessage; // ED: Added
   
    EDTargetManager *myTargetManager;
   
    BOOL gameOver;
    int myScreenWidth, myScreenHeight;
    int myTargetLifespan;
   
    int gameOverCounter; // ED: Added
    int gameScore; // ED: Added
}

@property (readonly, nonatomic, getter=isAnimating) BOOL animating;
@property (nonatomic) NSInteger animationFrameInterval;

- (void)startAnimation;
- (void)stopAnimation;

@end

We plan on having a score display at the top of the screen, so we’ll create a new myScoreMessage instance variable to hold a reference to our new score text.

The gameOverCounter variable will be used to create a delay between ending one game and starting another, and the gameScore variable will contain the numeric score we will use to format our score display EDTextString object.

The first change to the TouchTargetsViewController.m file is in the awakeFromNib method.

- (void)awakeFromNib
{
    EAGLContext *aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   
    if (!aContext)
        NSLog(@"Failed to create ES context");
    else if (![EAGLContext setCurrentContext:aContext])
        NSLog(@"Failed to set ES context current");
   
    self.context = aContext;
    [aContext release];
   
    [(EAGLView *)self.view setContext:context];
    [(EAGLView *)self.view setFramebuffer];
   
    myTextStringManager = [[EDTextStringManager alloc] initWithCharacterSheetName:@"FontCooperStd.png"];
   
    myStatusMessage = [[EDTextString alloc] initWithString:@"Touch to Start"];
    [myStatusMessage setPositionX:160 andY:240 andZ:0];
    [myStatusMessage setSize:20];
   
    [myTextStringManager addTextString:myStatusMessage];
   
    myScoreMessage = [[EDTextString alloc] initWithString:@"Score: 0"]; // ED: Added
    [myScoreMessage setPositionX:160 andY:25 andZ:0]; // ED: Added
    [myScoreMessage setSize:20]; // ED: Added
   
    [myTextStringManager addTextString:myScoreMessage]; // ED: Added
   
    myTargetManager = [EDTargetManager sharedInstance];
   
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &myScreenWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &myScreenHeight);

    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;
   
    gameOverCounter = -1; // ED: Added
}

The only changes we made to this method are to add the allocation and initialization of the new myScoreMessage instance variable, and to add it to the text string manager. We also set the gameOverCounter to -1 since we don’t need any game over processing the first time through.

The next set of changes are in the drawFrame method.

- (void)drawFrame
{
    [(EAGLView *)self.view setFramebuffer];
   
    glClearColor(0.98, 0.98, 0.98, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
   
    [myTargetManager drawAllTargets];
   
    [myTextStringManager drawAllTextStrings];
   
    if(gameOver == FALSE && [myTargetManager numActiveTargets] == 0) {
        gameOver = TRUE;
       
        //[myStatusMessage setString:@"Touch to Start"]; // ED: Removed
        //[myTextStringManager addTextString:myStatusMessage]; // ED: Removed
       
        if([myStatusMessage isAlive] == FALSE) { // ED: Added
            gameOverCounter = 100; // ED: Added
           
            [myStatusMessage setString:@"Game Over!"]; // ED: Added
            [myTextStringManager addTextString:myStatusMessage]; // ED: Added
        } // ED: Added
    } else if(gameOver == TRUE) { // ED: Added
        if(gameOverCounter > 0) { // ED: Added
            gameOverCounter--; // ED: Added
        } else if(gameOverCounter == 0) { // ED: Added
            [myStatusMessage setString:@"Touch to Start"]; // ED: Added
            gameOverCounter--; // ED: Added
        } // ED: Added
    }
   
    [(EAGLView *)self.view presentFramebuffer];
}

Now, instead of checking to see if our game is over and then immediately setting the status display back to “Touch to Start”, we change the text to “Game Over” first and use the gameOverCounter to create a delay. Here’s how it works:

   if(gameOver == FALSE && [myTargetManager numActiveTargets] == 0) {
        gameOver = TRUE;
       
        //[myStatusMessage setString:@"Touch to Start"]; // ED: Removed
        //[myTextStringManager addTextString:myStatusMessage]; // ED: Removed
       
        if([myStatusMessage isAlive] == FALSE) { // ED: Added
            gameOverCounter = 100; // ED: Added
           
            [myStatusMessage setString:@"Game Over!"]; // ED: Added
            [myTextStringManager addTextString:myStatusMessage]; // ED: Added
        } // ED: Added

If the gameOver flag is FALSE and the number of active targets is 0, the game must have just ended. If that’s the case, we set gameOver to TRUE. Next, instead of simply resetting the status text to “Touch to Start”, we check to see if the status message is alive.

Remember, when we start a new game, we set a lifespan on our status text so it’ll fade away. When it finishes fading away, it’s marked as dead and is removed from the text string manager. The first time through, though, we’ve got “Touch to Start” initialized from the awakeFromNib method. If we don’t check to make sure we’re not alive, we’ll display “Game Over” before we even start the first game.

If the game just ended and the myStatusMessage is not alive, set the gameOverCounter to 100 and set the myStatusMessage text to “Game Over”. When were finished with that, we add the myStatusMessage object back into the text string manager.

    } else if(gameOver == TRUE) { // ED: Added
        if(gameOverCounter > 0) { // ED: Added
            gameOverCounter--; // ED: Added
        } else if(gameOverCounter == 0) { // ED: Added
            [myStatusMessage setString:@"Touch to Start"]; // ED: Added
            gameOverCounter--; // ED: Added
        } // ED: Added

If the gameOver flag is set to TRUE, we need to check the gameOverCounter variable. If it’s greater than 0, it must have been set in order to create a delay between games, so we decrement it and do nothing else.

If the gameOverCounter is 0, it must be time to change the “Game Over” text to “Touch to Start”. We do so, but we also decrement the counter one last time to prevent the code from going through here over and over, continuously setting the myStatusMessage text to “Touch to Start”. This is also why we initialize the gameOverCounter variable to -1 in the awakeFromNib method.

This alone is not enough to create a delay between games, however, it only handles the timing of the status message change from “Game Over” to “Touch to Start”. We need additional code to actually put the delay into effect, and we’ll find that code in the next method, touchesBegan:withEvent:.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *t = [[touches allObjects] objectAtIndex:0];
    CGPoint p = [t locationInView:self.view];
   
    if(gameOver == TRUE) {
        // Game is over, user is touching to start. Generate five
        // targets in random locations.
       
        if(gameOverCounter > 0) { // ED: Added
            return; // ED: Added
        } // ED: Added
       
        gameScore = 0; // ED: Added
        [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added
       
        gameOver = FALSE;
        myTargetLifespan = 250;
       
        EDTarget *target;
       
        for(int i = 0; i < 5; i++) {
            target = [[EDTarget alloc] init];
           
            int targetSize = 64;  // Target width and height will match
           
            // The targets are centered, so adjust the position
            // by half of the width and height
           
            int minX = targetSize / 2;
            int maxX = myScreenWidth - (targetSize / 2);
           
            //int minY = targetSize / 2; // ED: Removed
            int minY = targetSize / 2 + 25; // ED: Added - account for score area at top of screen
            int maxY = myScreenHeight - (targetSize / 2);
           
            int randomX = arc4random() % (maxX - minX + 1) + minX;
            int randomY = arc4random() % (maxY - minY + 1) + minY;
           
            [target setPositionX:randomX andY:randomY andZ:0];
            [target setLifespan:myTargetLifespan withDecayAt:(myTargetLifespan / 2)];
            [target setTargetWidth:targetSize andHeight:targetSize];
           
            [myTargetManager addTarget:target];
        }
       
        // Also make "Touch to Start" text fade away.
       
        [myStatusMessage setLifespan:100 withDecayAt:100];
    } else {
        // Game is NOT over, so check to see if we hit a target
       
        EDTarget *target = [myTargetManager didHitTargetAtX:p.x andY:p.y];
       
        if(target != nil) {
            [target destroy]; // Remove hit target from game
           
            gameScore += 500; // ED: Added
            [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added
           
            if(myTargetLifespan > 5) {
                myTargetLifespan -= 5; // Decrease global lifespan
            }
           
            // Create new target to take hit target's place
           
            int targetSize = 64;  // Target width and height will match
           
            // The targets are centered, so adjust the position
            // by half of the width and height
           
            int minX = targetSize / 2;
            int maxX = myScreenWidth - (targetSize / 2);
           
            //int minY = targetSize / 2; // ED: Removed
            int minY = targetSize / 2 + 25; // ED: Added - account for score area at top of screen
            int maxY = myScreenHeight - (targetSize / 2);
           
            int randomX = arc4random() % (maxX - minX + 1) + minX;
            int randomY = arc4random() % (maxY - minY + 1) + minY;
           
            target = [[EDTarget alloc] init];
           
            [target setPositionX:randomX andY:randomY andZ:0];
            [target setLifespan:myTargetLifespan withDecayAt:(myTargetLifespan / 2)];
            [target setTargetWidth:targetSize andHeight:targetSize];
           
            [myTargetManager addTarget:target];
           
            // Pop up some floating text to let player know that the hit
            // was registered
           
            EDTextString *hitScore = [[EDTextString alloc] initWithString:@"+500"];
           
            [hitScore setColorRed:0 andGreen:128 andBlue:0];
            [hitScore setPositionX:p.x andY:p.y andZ:0];
            [hitScore setLifespan:100 withDecayAt:100];
            [hitScore setDriftX:0 andY:-1 andZ:0];
            [hitScore setSize:15];
           
            [myTextStringManager addTextString:hitScore];
        } else {
            // No target was hit, so display a penalty
           
            gameScore -= 100; // ED: Added
            [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added
           
            EDTextString *hitScore = [[EDTextString alloc] initWithString:@"-100"];
           
            [hitScore setColorRed:128 andGreen:0 andBlue:0];
            [hitScore setPositionX:p.x andY:p.y andZ:0];
            [hitScore setLifespan:100 withDecayAt:100];
            [hitScore setDriftX:0 andY:-1 andZ:0];
            [hitScore setSize:15];
           
            [myTextStringManager addTextString:hitScore];
        }
    }    
}

There are really only a few spots we need to look at, but they might not make sense if isolated, so I’ve listed the entire method for reference.

The first change is right after we detect that there’s no game in progress, after the first if statement.

        if(gameOverCounter > 0) { // ED: Added
            return; // ED: Added
        } // ED: Added

Normally, if there’s no game in progress, a touch will start a new game. Since we want a delay between “Game Over” and “Touch to Start”, we code it here, in front of the logic that’s used to start a new game. If the gameOver counter is greater than zero, this method will return without processing touches, preventing a new game from starting during the “Game Over” message.

When the gameOverCounter finally is zero, and the player touches the screen, a new game will begin, and execute our next bit of new code.

        gameScore = 0; // ED: Added
        [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added

This code simply resets the score counter and updates the score display message.

            //int minY = targetSize / 2; // ED: Removed
            int minY = targetSize / 2 + 25; // ED: Added - account for score area at top of screen

Since we’ve added a score message at the top of the screen, we don’t want targets being drawn up there, so we add a little padding to the minimum Y position for generating random targets.

            gameScore += 500; // ED: Added
            [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added

In the code that registers a hit, we now increment the score counter and update our score display message.

            //int minY = targetSize / 2; // ED: Removed
            int minY = targetSize / 2 + 25; // ED: Added - account for score area at top of screen

Since we pop a new target when an existing target its hit, we need to add the same adjustment code from earlier so new targets wont be drawn in the score display area.

            gameScore -= 100; // ED: Added
            [myScoreMessage setString:[[NSString alloc] initWithFormat:@"Score: %d", gameScore]]; // ED: Added

When a target is missed, no new targets are created, so we just need to decrement our score counter and update the score display.

That’s all of the changes to implement scoring, so let’s compile and run the app and see what happens.

 

 

Now we can hit targets (or miss) and the game will keep track of our score.

Chapter 12 | Index | Chapter 14