Chapter 12: Fire!

In the last chapter, we created the EDTarget and EDTargetManager classes, so we now have all we need to draw targets on the screen and register hits. The only thing left to do before our demo is ready is to make a few changes to the view controller.

Let’s start by looking at the header file, TouchTargetsViewController.h.

#import <UIKit/UIKit.h>

#import <OpenGLES/EAGL.h>

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

#import "EDTextStringManager.h"
#import "EDTargetManager.h" // ED: Added

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

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

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

@end

Since we’ll be taking advantage of the EDTarget and EDTargetManager classes in the view controller, we’ll add an #import statement for EDTargetManager.h (which includes an #import statement for EDTarget.h).

We’ll also want to display a status message, “Touch to Start”, and we’re going to want to have control over it. We’ll display the message when there’s no game in progress, and fade it our after a game starts. When the game is over, we’ll display it again. The easiest way to do all of this is to maintain an instance variable for this text string. The myStatusMessage instance variable will be used for this purpose.

The myTargetManager instance variable will hold a reference to our target manager. The gameOver switch will allow us to keep track of whether or not we’re currently playing a game, and the myScreenWidth and myScreenHeight variables will store our iPhone screen dimensions.

We didn’t care about screen dimensions before because we were just drawing text wherever the user touched, and since the user can’t register a touch off of the screen, there was no problem. Now, however, we’re going to draw targets at random locations around the screen, so we’ll need to know our boundaries.

Finally, the myTargetLifespan will be used to set how quickly the targets will fade after being drawn. Remember, our targets will begin to fade shortly after begin drawn, and if they aren’t touched, will disappear for the rest of the game. If they are touched, they will disappear and a new target will pop up with a slightly shorter lifespan.

That covers the new code in the header file, let’s look at the TouchTargetsViewController.m file. We’ll only review code that’s changed.

- (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"];
   
    /* ED: Removed
    EDTextString *myTextString = [[EDTextString alloc] initWithString:@"Hello, World!"];
    [myTextString setPositionX:160 andY:240 andZ:0];
    [myTextString setColorRed:0 andGreen:0 andBlue:128];
    [myTextString setSize:20];
   
    [myTextStringManager addTextString:myTextString];
     */

   
    myStatusMessage = [[EDTextString alloc] initWithString:@"Touch to Start"]; // ED: Added
    [myStatusMessage setPositionX:160 andY:240 andZ:0]; // ED: Added
    [myStatusMessage setSize:20]; // ED: Added
   
    [myTextStringManager addTextString:myStatusMessage]; // ED: Added
   
    myTargetManager = [EDTargetManager sharedInstance]; // ED: Added
   
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &myScreenWidth); // ED: Added
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &myScreenHeight); // ED: Added

    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;
}

We need to remove the code we were using to create the “Hello, World!” message. We can’t reuse it for our “Touch to Start” message because we need to keep a reference to that EDTextString object, and the myTextString variable here was declared locally.

    myStatusMessage = [[EDTextString alloc] initWithString:@"Touch to Start"]; // ED: Added
    [myStatusMessage setPositionX:160 andY:240 andZ:0]; // ED: Added
    [myStatusMessage setSize:20]; // ED: Added
   
    [myTextStringManager addTextString:myStatusMessage]; // ED: Added

The next few lines allocates and initializes our myStatusMessage instance variable to “Touch to Start”. After setting a few text attributes, we add it to the text string manager.

Even if it’s removed from the text string manager, it will not be released since we have a reference to it via our instance variable.

    myTargetManager = [EDTargetManager sharedInstance]; // ED: Added

The next line grabs the shared instance of the EDTargetManager. Behind the scenes, the sharedInstance method in the EDTargetManager class will take care of allocating and initializing the shared instance of this class for us, if needed, but everyone else should always use the sharedInstance class method to get to this object.

    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &myScreenWidth); // ED: Added
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &myScreenHeight); // ED: Added

The only other code we added was to get our width and height from OpenGL.

There are also a few changes needed to 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]; // ED: Added
   
    [myTextStringManager drawAllTextStrings];
   
    if(gameOver == FALSE && [myTargetManager numActiveTargets] == 0) { // ED: Added
        gameOver = TRUE; // ED: Added
       
        [myStatusMessage setString:@"Touch to Start"]; // ED: Added
        [myTextStringManager addTextString:myStatusMessage]; // ED:Added
    } // ED: Added
   
    [(EAGLView *)self.view presentFramebuffer];
}

We add a call to the drawAllTargets method of our myTargetManager object so that our targets will be drawn onto the screen. It’s important to note the order of draw requests, too. Notice that we added the call to draw the targets before the existing call to draw all text strings.

The reason for that is because OpenGL draws things in order, so by drawing text after we draw targets, we’ll make sure that our text is drawn on top of any targets. Later, when we draw a background, we’ll need to make sure it’s the first things drawn so that everything else will be in front of it.

We also added a conditional block of code.

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

The conditional statement is checking to see if a game is in progress (gameOver == FALSE) and if the number of targets is 0. If this is the case, the player must have just finished up a game.

If the conditional is satisfied, we set gameOver to TRUE, and set our myStatusMessage text string to “Touch to Start”. After that, we add the myStatusMessage object back into the text string manager.

Before you go looking through the code to see where it changed to something else from “Touch to Start”, allow me to save you some time; it didn’t.

However, as we’ll see in the next method we cover, we faded it out when the game started, resulting in it being removed from the text manager. If we don’t call the setString method, the text string will still consider itself dead. The setString method resets the alive property to “TRUE”, alpha to 1 (so it’s fully visible again), and lifespan to 0 (live forever).

Executing this logic will result in the “Touch to Start” message being redisplayed on the iPhone screen after a game ends.

Before we look at the new touchesBegan:withEvent: method, I want to mention that we’re completely commenting out the existing touchedEnded:withEvent: method, like so.

/* ED: Removed method
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    // If user taps, generate a new text string with a few
    // attributes randomized.
   
    const NSArray *colorNameArray = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", @"Yellow", @"Purple", @"Cyan", nil];
    static const int colorValueArray[] = { 128,0,0, 0,128,0, 0,0,128, 128,128,0, 128,0,128, 0,128,128 };
   
    UITouch *t = [[touches allObjects] objectAtIndex:0];
    CGPoint p = [t locationInView:self.view];
   
    int randomInt = (arc4random() % (5 - 0 + 1)) + 0;
    int randomDrift = (arc4random() % (3 - 1 + 1)) + 1;
    int randomSize = (arc4random() % (40 - 20 + 1)) + 20;
   
    EDTextString *textString = [[EDTextString alloc] initWithString:[colorNameArray objectAtIndex:randomInt]];
   
    [textString setPositionX:p.x andY:p.y andZ:0];
    [textString setColorRed:colorValueArray[randomInt * 3] andGreen:colorValueArray[randomInt * 3 + 1] andBlue:colorValueArray[randomInt * 3 + 2]];
    [textString setDriftX:0 andY:-randomDrift andZ:0];
    [textString setLifespan:100 withDecayAt:50];
    [textString setSize:randomSize];
   
    [myTextStringManager addTextString:textString];
}
 */

The reason we’re removing the touchesEnded:withEvent: method and replacing it with the touchesBegan:withEvent: method is timing.

When you first touch the iPhone screen, it sends a touchesBegan:withEvent: message. If you hold your finger down and move it, the iPhone sends you touchesMoved:withEvent: messages. When you’re finished doing whatever you’re doing and lift your finger, the iPhone sends the touchesEnded:withEvent: message.

For drawing text on the screen wherever a user has touched, the touchesEnded:withEvent: worked fine, but for a fast application where you’re trying to tap targets before they disappear, timing matters. By using the touchesBegan:withEvent: message, we can give the player every advantage by detecting the hit when they first touch the screen, and not waiting until they lift a finger.

Let’s look at our new touchesBegan:withEvent: method now.

// ED: Added method

- (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.
       
        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;
            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
           
            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;
            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
           
            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];
        }
    }
   
}

While there is a lot of code in this new method, it’s all pretty straightforward. Let’s look at it one functional chunk at a time.

    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.
       
        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;
            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];

If gameOver is TRUE, that means we’re touching to start a new game. That being the case, we set gameOver to FALSE, set the myTargetLifespan to 250, and generate our five targets.

Because the targets are centered over their coordinates, we adjust the minimum and maximum X and Y coordinates by half of the width of the target. After that, we generate random coordinates with our old friend, the arc4random() function.

After setting our target attributes, we add it to our target manager and continue looping.

After the five targets have been created and added to the target manager, we set the lifespan and decay of our status message to 100 so it will fade out and disappear as the game starts.

    } 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
           
            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;
            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];

If the gameOver was not TRUE, that means we’re in a game right now, and we need to see if the player touched a target.

The didHitTargetAtX:andY: method in the EDTargetManager class will return a reference to the hit target if it determines that the target was, in fact, hit. If we get an EDTarget object back, we know that was the target that got hit, so we call its destroy method to get rid of it.

Since a target has been hit, we decrement the global target lifespan to keep things interesting and create a new target to take its place. The code to add a new target is identical to the code we used while we were creating five new targets when the game started.

After the target has been added to the target manager, we create some floating green score text to let the player know that the hit was registered successfully and add it to the text manager. The text will be added to whatever location the player touched.

        } else {
            // No target was hit, so display a penalty
           
            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];
        }

If the return from EDTargetManager’s didHitTargetAtX:andY: was nil, that tells us that no target was hit, so all we need to do is create some loading red score penalty text wherever the player touched and add it to the text string manager. No targets need to be destroyed or created.

And that’s it.

Before we compile and run the app, we should do one final thing. The red and white target image would make a nice app icon, so let’s add that now. After creating a 57×57 pixel version of the target graphic named icon.png, we open our TouchTargets-info.plist file and update the “Icon File” property.

 

 

Now when we compile and run the demo app, we’ll have a nice target icon to go along with it.

After compiling and running the code, we get a nice target touch demo game.

 

 

We’ve got the targets being managed properly and floating text when targets are hit or missed. We can tell by the floating text that the hits and misses are being registered correctly, so we really only have a background and a scoring mechanism missing to make this a real game.

The next job we’re going to take on will be putting a scoring mechanism in place, and then after that we’ll finish up the project with a bright, colorful scrolling background.

Chapter 11 | Index | Chapter 13