Chapter 11: Aim…

Now that the view controller has been cleaned up, let’s look into our new target and target manager classes. Since we’ve already been through the EDTextString class in great detail, I don’t think we’ll have any problems understanding what the new EDTarget class is doing. Let’s take a look at the EDTarget class header file.

#import <Foundation/Foundation.h>

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

@interface EDTarget : NSObject {
    GLfloat *vertexArray;       // Array of target vertices
    GLfloat *textureCoordArray; // Array of texture coordinates
   
    GLfloat position[3];    // X, Y, & Z coordinates of target
   
    BOOL alive;         // Target active or not
   
    int myScreenWidth;    // Screen width
    int myScreenHeight;   // Screen height
   
    float lifespan; // How long this target should live
    float lifeleft; // How much life this target has left
    float decayAt;  // When this target should start to decay
   
    float alpha;    // Opacity
   
    int myWidth;    // This target's width
    int myHeight;   // This target's height
}

- (EDTarget *)init;

- (void)setPositionX:(int)x andY:(int)y andZ:(int)z;
- (void)setLifespan:(int)newLifespan withDecayAt:(int)newDecayAt;
- (void)setTargetWidth:(int)newWidth andHeight:(int)newHeight;

- (BOOL)isAlive;

- (float *)getOpenGLVertexArray;
- (float *)getOpenGLTextureCoordArray;
- (int)getOpenGLNumVertices;
- (BOOL)didHitAtX:(int)x andY:(int)y;
- (void)destroy;
- (float)getAlpha;

- (void)populateVertexArray;
- (void)populateTextureCoordArray;
- (void)update;

@end

There are a lot of similarities between this class and the EDTextString class, and that’s because they’re both classes designed to be used with a manager class. Since the manager classes will be expected to handle drawing these objects, we see the return of the vertex and texture coordinate arrays. But since the managers are not responsible for tracking each worker object’s lifespan, position on the screen, and similar per-object properties, we see some familiar instance variables in here too.

There are some differences as well. Since the size of the text strings were dependent upon the sizes and dimensions of the individual characters in the source font, we were able to make a lot of calculations in the EDTextString code. Here, we can explicitly set the height and width of the target, and they can even be different.

We also have a method that reports whether or not a particular screen position is within the bounds of the target, which will be used by the manager to find any targets that might have been touched by the player. Based on the integer types of the input parameters to this method, we can guess that we’ll be passing in screen coordinates which will need to be converted to OpenGL coordinates.

Let’s look at the EDTarget.m source file.

#import "EDTarget.h"

@implementation EDTarget

- (EDTarget *)init {
    if((self = [super init])) {
        vertexArray = NULL;
        textureCoordArray = NULL;
       
        position[0] = position[1] = position[2] = 0;
       
        alive = TRUE;
       
        myWidth = myHeight = 64; // Default target size to 64x64
       
        alpha = 1;  // Default to full opacity
       
        lifespan = 0;   // Live forever
        lifeleft = 0;   // Default to lifeleft
        decayAt = 0;    // Default to 0

        // Get screen resolution from OpenGL
       
        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &myScreenWidth);
        glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &myScreenHeight);
    }
   
    return self;
}

At the top of the file, we have a pretty basic init method that’s setting some object defaults. We see that this class is also using the glGetRenderbufferParameteriv() function to get the host context’s renderbuffer width and height, so we’ll need to make sure that the EAGLView OpenGL ES 2.0 initialization has been performed before instantiating this class.

The next several methods are identical in function to the same methods we’ve already discussed in Chapter 7: Creating the Text String Class, so I’ll just list them here for reference.

// The setPositionX:andY:andZ: method will take screen coordinates and store them as
// OpenGL coordinates to use later.

- (void)setPositionX:(int)x andY:(int)y andZ:(int)z {
    // The positions will be held in OpenGL coordinates, so we need to
    // convert them.
   
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    position[0] = conversionXFactor * ((float)x - ((float)myScreenWidth / 2.0)); // X
    position[1] = conversionYFactor * ((float)y - ((float)myScreenHeight / 2.0)) * -1; // Y (inverted)
    position[2] = 0; // Ignore Z for now, but code for it later    
}

// The setLifespan:withDecayAt: method simply sets the lifespan and decay values

- (void)setLifespan:(int)newLifespan withDecayAt:(int)newDecayAt {
    lifespan = newLifespan;
    decayAt = newDecayAt;
   
    lifeleft = lifespan; // Default lifeleft to full lifespan
}

// Sets the target width and height

- (void)setTargetWidth:(int)newWidth andHeight:(int)newHeight {
    myWidth = newWidth;
    myHeight = newHeight;
}

// Return alive indicator

- (BOOL)isAlive {
    return alive;
}

// Call populateVertexArray every time in case the position has changed

- (float *)getOpenGLVertexArray {
    [self populateVertexArray];
    return vertexArray;
}

// If needed, call populateTextureCoordArray

- (float *)getOpenGLTextureCoordArray {
    if(textureCoordArray == NULL) {
        [self populateTextureCoordArray];
    }
   
    return textureCoordArray;
}

// Targets are made up of two triangle, each with three vertices, so the
// return from this method will always be six

- (int)getOpenGLNumVertices {
    return 6;
}

The next method is new, so we’ll go over that one in detail.

// Check to see if the passed coordinates are inside of this target

- (BOOL)didHitAtX:(int)x andY:(int)y {
    // Get screen coordinate to OpenGL conversion factor
   
    float conversionXFactor = 2.0 / (float)myScreenWidth;
    float conversionYFactor = 2.0 / (float)myScreenHeight;
   
    // Apply conversion factors to passed in coordinates so we can compare them
    // to our on position coordinates
   
    float convertedX = conversionXFactor * ((float)x - ((float)myScreenWidth / 2.0));
    float convertedY = conversionYFactor * ((float)y - ((float)myScreenHeight / 2.0)) * -1; // invert Y
   
    float left = position[0] - (conversionXFactor * (float)(myWidth / 2));
    float right = position[0] + (conversionXFactor * (float)(myWidth / 2));
    float bottom = position[1] - (conversionYFactor * (float)(myHeight / 2));
    float top = position[1] + (conversionYFactor * (float)(myHeight / 2));
   
    if(convertedX >= left && convertedX <= right && convertedY >= bottom && convertedY <= top) {
        return TRUE;
    }
   
    return FALSE;
}

First, we create a conversion factor based on iPhone screen space to OpenGL space, then use that factor to determine where in OpenGL space those iPhone screen coordinate lie.

Once we know that, we calculate the left side, right side, bottom, and top of ourself (an EDTarget object) based on the conversion factors and our own width and height.

Knowing where in OpenGL space the player touched, and knowing our own OpenGL space boundaries, all we need to do is check to see if the point the player touched in within our bounds. If it is, we return TRUE, if it’s not, we return FALSE.

The next two methods should also look familiar, and don’t need any explanation.

// Set alive flag to FALSE

- (void)destroy {
    alive = FALSE;
}

// Return my alpha (opacity) value

- (float)getAlpha {
    return alpha;
}

Like the text string objects, the target objects will need to produce their own vertex and texture coordinate arrays. Unlike the text string objects, the target objects have a very simple job. All the target objects need to do is generate a vertex array based on their current location, and a texture coordinate array that’s hardcoded. We’re using one complete texture for the target face, so we just map the whole thing.

// We need a couple of triangles to make the square that will
// make up our target. This method will create a vertex array for
// rendering under OpenGL.

- (void)populateVertexArray {
    // Walk through internal text string and load arrays
   
    float x, y, z; // x, y, and z vertex coordinates
   
    if(vertexArray != NULL) {
        free(vertexArray);
    }
   
    vertexArray = malloc(sizeof(float) * 6 * 3);
   
    x = position[0];
    y = position[1];
    z = position[2];
   
    float screenXConversion = 2.0 / ((float)myScreenWidth);
    float screenYConversion = 2.0 / ((float)myScreenHeight);
   
    // Adjust for centering
   
    x = x - (screenXConversion * (float)(myWidth / 2));
    y = y - (screenYConversion * (float)(myHeight / 2));
   
    int vertexIndex = 0;
   
    vertexArray[vertexIndex     ] = x;
    vertexArray[vertexIndex + 1 ] = y;
    vertexArray[vertexIndex + 2 ] = z;
   
    x += myWidth * screenXConversion;
   
    vertexArray[vertexIndex + 3 ] = x;
    vertexArray[vertexIndex + 4 ] = y;
    vertexArray[vertexIndex + 5 ] = z;
   
    x -= myWidth * screenXConversion;
    y += myHeight * screenYConversion;
   
    vertexArray[vertexIndex + 6 ] = x;
    vertexArray[vertexIndex + 7 ] = y;
    vertexArray[vertexIndex + 8 ] = z;
   
    x += myWidth * screenXConversion;
   
    vertexArray[vertexIndex + 9 ] = x;
    vertexArray[vertexIndex + 10] = y;
    vertexArray[vertexIndex + 11] = z;
   
    x -= myWidth * screenXConversion;
   
    vertexArray[vertexIndex + 12] = x;
    vertexArray[vertexIndex + 13] = y;
    vertexArray[vertexIndex + 14] = z;
   
    x += myWidth * screenXConversion;
    y -= myHeight * screenYConversion;
   
    vertexArray[vertexIndex + 15] = x;
    vertexArray[vertexIndex + 16] = y;
    vertexArray[vertexIndex + 17] = z;
   
}

// The populateTextureCoordArray should only need to be called
// once - the texture coordinates will not be changing much, if
// at all.

- (void)populateTextureCoordArray {
    // Walk through internal text string and load arrays
   
    if(textureCoordArray != NULL) {
        free(textureCoordArray);
    }
   
    textureCoordArray = malloc(sizeof(float) * 6 * 2);
   
    textureCoordArray[0     ] = 0;
    textureCoordArray[0 + 1 ] = 0;
   
    textureCoordArray[0 + 2 ] = 1;
    textureCoordArray[0 + 3 ] = 0;
   
    textureCoordArray[0 + 4 ] = 0;
    textureCoordArray[0 + 5 ] = 1;
   
    textureCoordArray[0 + 6 ] = 1;
    textureCoordArray[0 + 7 ] = 1;
   
    textureCoordArray[0 + 8 ] = 0;
    textureCoordArray[0 + 9 ] = 1;
   
    textureCoordArray[0 + 10] = 1;
    textureCoordArray[0 + 11] = 0;                
}

Finally, we have our update method. We don’t drift, so all we need to do is adjust our alpha if needed.

// Update state of target

- (void)update {
    if(lifespan > 0) {
        lifeleft--;
       
        if(lifeleft <= 0) {
            alive = FALSE;
        } else {
            if(lifeleft <= decayAt) {
                // It's time to start decaying
                alpha = lifeleft / decayAt;
            }
        }
    }
   
    return;
}

@end

That was so easy that I think we’ll go ahead and cover the target manager class as well in this chapter.

Like the EDTextStringManager class, the EDTargetManager will be responsible for loading any needed OpenGL textures, managing multiple targets, and drawing them on the screen.

Let’s have a look at the header file, EDTargetManager.h.

#import <Foundation/Foundation.h>

#import "EDOpenGLProgram.h"
#import "EDTarget.h"
#import "EDOpenGLTools.h"

@interface EDTargetManager : NSObject {
    EDOpenGLProgram *myProgram; // OpenGL program object
   
    GLuint myTexture;   // OpenGL texture name for target graphic
   
    NSMutableArray *myTargetArray;  // Array of managed targets
}

+ (EDTargetManager *)sharedInstance;
- (EDTargetManager *)init;

- (void)addTarget:(EDTarget *)newTarget;
- (void)drawTarget:(EDTarget *)target;
- (void)drawAllTargets;
- (void)destroyAllTargets;

- (EDTarget *)didHitTargetAtX:(int)x andY:(int)y;
- (int)numActiveTargets;

@end

This also looks very similar to the text string manager, but there is one important difference.

See that first method, sharedInstance? It has a plus sign in front of it, which means that it’s a class instance. This class does not have to be instantiated to use it, but why is it there?

The text manager class took the name of a font sheet as an initialization parameter, which means that the text strings that it manages will be rendered in that font. If I wanted multiple fonts on the screen at the same time, I would need multiple managers, since using one manager and continuously switching font sheets would be a waste of resources.

For the target manager, however, we only ever want one around at a time. Since I’ll be scoring off of what the user hits and misses, I don’t want more than one target manager at a time, it would make scoring too difficult. Designing a class to allow only one instance as a time is called a singleton class. A singleton class is designed to have only one instance in a system. The way we accomplish that here is by giving the program a different way to access the singleton class.

Since it’s the first method in the class source file, let’s look at it now.

#import "EDTargetManager.h"

enum {
    ATTRIBUTE_VERTEX,
    ATTRIBUTE_TEXTURE_COORD,
    NUM_ATTRIBUTES
};
GLint myAttributes[NUM_ATTRIBUTES];

enum {
    UNIFORM_TEXTURE,
    UNIFORM_ALPHA,
    NUM_UNIFORMS
};
GLint myUniforms[NUM_UNIFORMS];

@implementation EDTargetManager

// We're only allowing one target manager, so don't allocate a new one, just return
// the one that's already managing targets.

+ (EDTargetManager *)sharedInstance {
    static EDTargetManager *sharedInstance;
   
    @synchronized(self) {
        if(!sharedInstance) {
            sharedInstance = [[EDTargetManager alloc] init];
        }
    }
   
    return sharedInstance;
}

After the familiar attribute and uniform arrays, we see the sharedInstance method. When called, this method will check to see if the static EDTargetManager object named sharedInstance is allocated or not, and create a new one only if needed. If one does exist, it is returned instead of creating another one.

This method will look very strange to anyone not familiar with this type of construct, so let’s go over it line by line.

+ (EDTargetManager *)sharedInstance {
    static EDTargetManager *sharedInstance;

The plus sign means that this method is not called off of an instantiated object, but off of the class itself. For example, I can do this:

EDTargetManager *myTargetManager = [EDTargetManager sharedInstance];

But I cannot do this:

EDTargetManager *myOtherTargetManager = [myTargetManager sharedInstance];

The static keyword in front of the EDTargetManager declaration means that the sharedInstance variable will persist even after this method returns. The next time someone calls this method, the sharedInstance variable will be the same as it was last time the method was called. Keeping the value of the sharedInstance variable is what makes this singleton process work.

    @synchronized(self) {
        if(!sharedInstance) {
            sharedInstance = [[EDTargetManager alloc] init];
        }
    }

Here’s an interesting bit of code. It’s pretty clear that the innermost code is checking to see if our static sharedInstance variable is valued (that is, not nil), and if it’s not, allocating and initializing it.

But what is that @synchronized(self) bit? Like @synthesize, the @synchronized directive will be expanded into code when the program is compiled. Exactly what that code consists of doesn’t concern us, but just know that @synchronized will be turned into a block of more complex code.

What that code will do is guarantee that only one thread can call the code within it at a time. Imagine this: two different threads want a reference to our target manager’s shared instance at the exact same time. If they both executed this line:

if(!sharedInstance) {

at the exact same time, they would both be told that it is not valued. As a result, we would get two threads executing the next line at the exact same time:

sharedInstance = [[EDTargetManager alloc] init];

Since the sharedInstance variable is static, it can’t be two things at the same time, so the processing would be undefined.

To prevent that, we use the @synchronized keyword to set up a gate, of sorts. The object in parentheses, self, is used a flag to signal if someone is executing the code. This flag is known as a mutex object, which stands for mutual exclusion object. When two threads try to execute this code at the same time, only one of them will get in and set the mutex. The other will be blocked, and forced to wait until the mutex is clear.

Using the mutex, we can guarantee that only one thread at a time will be able to execute this code, and that there will always only be one instance of the EDTargetManager around at the same time.

    return sharedInstance;
}

Finally, we return the sharedInstance variable.

Even though we’re a singleton, we still have a very familiar init method. The difference is that the init method isn’t called by anyone except our own sharedInstance method.

// Standard initialization routine

- (EDTargetManager *)init {
    if((self = [super init])) {
        // Load target texture
       
        [EDOpenGLTools loadTexture:&myTexture fromFile:@"Target.png"];
       
        // Load and initialize OpenGL program
       
        myProgram = [[EDOpenGLProgram alloc] init];
       
        [myProgram setVertexShader:@"EDTargetManager"];
        [myProgram setFragmentShader:@"EDTargetManager"];
       
        [myProgram addAttributeLocation:@"ATTRIBUTE_VERTEX" forAttribute:@"vertex"];
        [myProgram addAttributeLocation:@"ATTRIBUTE_TEXTURE_COORD" forAttribute:@"texture_coord"];
       
        [myProgram addUniformLocation:@"UNIFORM_TEXTURE" forUniform:@"texture"];
        [myProgram addUniformLocation:@"UNIFORM_ALPHA" forUniform:@"alpha"];
       
        [myProgram compileAndLink];
       
        myAttributes[ATTRIBUTE_VERTEX] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_VERTEX"];
        myAttributes[ATTRIBUTE_TEXTURE_COORD] = [myProgram getAttributeIDForIndex:@"ATTRIBUTE_TEXTURE_COORD"];
       
        myUniforms[UNIFORM_TEXTURE] = [myProgram getUniformIDForIndex:@"UNIFORM_TEXTURE"];
        myUniforms[UNIFORM_ALPHA] = [myProgram getUniformIDForIndex:@"UNIFORM_ALPHA"];
       
        myTargetArray = NULL;
    }
   
    return self;
}

In a nearly identical fashion to our EDTextStringManager, the EDTargetManager is loading a texture, creating an OpenGL program with custom shaders, and initializing the internal array of managed objects to nil.

The next several methods are so familiar that I’ll just list them for reference, I don’t think you’ll have any problem recognizing what they do.

// Add a target to my array of managed targets

- (void)addTarget:(EDTarget *)newTarget {
    if(myTargetArray == NULL) {
        myTargetArray = [[NSMutableArray alloc] init];
    }
   
    [myTargetArray addObject:newTarget];
}

// Draw a specific target

- (void)drawTarget:(EDTarget *)target {
    glUseProgram([myProgram programId]);
   
    glBindTexture(GL_TEXTURE_2D, myTexture);
   
    glUniform1i(myUniforms[UNIFORM_TEXTURE], 0);
    glUniform1f(myUniforms[UNIFORM_ALPHA], [target getAlpha]);
   
    glVertexAttribPointer(myAttributes[ATTRIBUTE_VERTEX], 3, GL_FLOAT, 0, 0, [target getOpenGLVertexArray]);
    glEnableVertexAttribArray(myAttributes[ATTRIBUTE_VERTEX]);
   
    glVertexAttribPointer(myAttributes[ATTRIBUTE_TEXTURE_COORD], 2, GL_FLOAT, 0, 0, [target getOpenGLTextureCoordArray]);
    glEnableVertexAttribArray(myAttributes[ATTRIBUTE_TEXTURE_COORD]);
   
    // Validate program before drawing. This is a good check, but only really necessary in a debug build.
    // DEBUG macro must be defined in your debug configurations if that's not already the case.
#if defined(DEBUG)
    if (![myProgram validate]) {
        NSLog(@"Failed to validate program: %d", [myProgram programId]);
        return;
    }
#endif
   
    glDrawArrays(GL_TRIANGLES, 0, [target getOpenGLNumVertices]);
   
    [target update];
}

// Draw all managed targets

- (void)drawAllTargets {
    if(myTargetArray == NULL) {
        return;
    }
   
    for(int i = [myTargetArray count] - 1; i >= 0; i--) {
        if(![[myTargetArray objectAtIndex:i] isAlive]) {
            [myTargetArray removeObjectAtIndex:i];
        }
    }
   
    for(int i = 0; i < [myTargetArray count]; i++) {
        [self drawTarget:[myTargetArray objectAtIndex:i]];
    }
}

// Destroy all targets

- (void)destroyAllTargets {
    [myTargetArray removeAllObjects];
}

The only methods with functionality that we haven’t seen before are the didHitTargetAtX:andY method and the numActiveTargets method.

// Find the first target that would be hit by the passed in coordinates. Since we'll
// be drawing targets from index 0 through the last, we'll search for hits backwards, since
// the higher numbered targets will be being drawn on top of the lower numbered ones. If there
// are more than one possible targets hit at a set of coordinates, we want the one that was
// drawn on top of the others to register the hit.

- (EDTarget *)didHitTargetAtX:(int)x andY:(int)y {
    for(int i = [myTargetArray count] - 1; i >= 0; i--) {
        EDTarget *target = [myTargetArray objectAtIndex:i];
       
        if([target didHitAtX:x andY:y]) {
            return target;
        }
    }
   
    return nil;
}

Since we’ve already done all of that work in the EDTarget class to detect a hit, we’re just going to loop through the targets we’re managing and ask each one if the coordinates we got hit them.

One thing worth nothing, though, is that since OpenGL draws things in order, the first targets in this array will have been drawn first, with later targets being drawn over them if they overlapped. If the user touches an area on the screen that could count as a hit for more than one target, we want the target drawn last to register the hit, not the target or targets drawn before it. If we don’t use the target drawn last, it will appear as though the user is magically hitting targets behind other targets without touching the target or targets in front of it.

// return the number of targets still alive

- (int)numActiveTargets {
    return [myTargetArray count];
}

@end

The final method in the class simply returns the number of targets still in the managed array. This method will be needed to detect when all targets have disappeared.

The last thing we need to worry about are the shaders.

attribute vec4 vertex;
attribute vec4 texture_coord;

varying vec4 varTextureCoord;

void main()
{
    gl_Position = vertex;
   
    varTextureCoord = texture_coord;
}

Standard vertex shader, nothing to discuss here.

varying highp vec4 varTextureCoord;

uniform sampler2D texture;
uniform mediump float alpha;

void main()
{
    mediump vec4 frag_color = texture2D(texture, varTextureCoord.st);
   
    frag_color.a = alpha;
   
    gl_FragColor = frag_color;
}

The EDTargetManager fragment shader is similar to the EDTextStringManager fragment shader, except that we don’t set any colors, just the alpha value.

If you create your own shaders, don’t forget to remove them from the Copy Sources build phase and add them to the Copy Bundle Resources build phase.

Now that we have the EDTarget class and EDTargetManager classes ready to go, we can create a new iPhone demo that will display targets and detect hits, complete with floating score displays.

Chapter 10 | Index | Chapter 12