How To Create A Simple 2D iPhone Game with OpenGL ES 2.0 and GLKit – Part 2

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. In this tutorial series, we are creating a very simple game for the iPhone using the lowest level APIs on iOS – OpenGL ES 2.0 and GLKit. In the first part of the series, we created a basic OpenGL […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

The Great Refactor

The only step remaining in our game is to handle the win/lose condition. When we did this with Cocos2D, we would simply switch to a different game scene that would display some text showing if the user won or lost, and then restart the game.

That was trivial with Cocos2D, but would be a bit of a pain now. Right now we’ve hard-coded all of our game logic into the SGGViewController.m, which kind of acts like the “one true scene.”

We could cheat and add logic into the SGGViewController.m to clear out all the nodes, display a win/lose, and restart the game, but it would be better to refactor our code a bit into using a node hierarchy.

Right now the SGGViewController.m contains a list of sprites to display. But why not make it so that it just has a root “node” to display, and that “node” can have children, and so on? This way we can have a node that is a “scene” that contains the action scene, and another for the “game over” scene.

This is also handy for situations where you want to have other parent/child relationships, so that moving the parent node moves the child node, and so on.

There’s a good bit of refactoring to do, so if you want to call it a day at this point, here’s the finished project without refactoring.

Otherwise, read on to learn about creating node hierarchies and scenes!

Sprites to Nodes

The first thing we’re going to do is create a class for a node, and move some of the code that’s currently in the SGGViewController and the SGGSprite to this new class.

Create a new file with the iOS\Cocoa Touch\Objective-C class template. Enter SGGNode for the Class, NSObject for the Subclass, click Next, and click Create.

Open SGGNode.h and replace it with the following:

#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>

@interface SGGNode : NSObject

@property (assign) GLKVector2 position;
@property (assign) CGSize contentSize;
@property (assign) GLKVector2 moveVelocity;
@property (retain) NSMutableArray * children;

- (void)renderWithModelViewMatrix:(GLKMatrix4)modelViewMatrix; 
- (void)update:(float)dt;
- (GLKMatrix4) modelMatrix:(BOOL)renderingSelf;
- (CGRect)boundingBox;
- (void)addChild:(SGGNode *)child;

@end

Here we bring in several properties from SGGSprite (position, contentSize, and moveVelocity), as well as the children property from SGGViewController.

We also bring in some methods from SGGSprite (render, update, modelMatrix, boundingBox). Note that render has been modified to take a model view matrix as parameter, and modelMatrix has a flag – more on this later.

Next open SGGNode.m and replace it with the following:

#import "SGGNode.h"

@implementation SGGNode
@synthesize position = _position;
@synthesize contentSize = _contentSize;
@synthesize moveVelocity = _moveVelocity;
@synthesize children = _children;

- (id)init {
    if ((self = [super init])) {
        self.children = [NSMutableArray array];
    }
    return self;
}

- (void)renderWithModelViewMatrix:(GLKMatrix4)modelViewMatrix {
    GLKMatrix4 childModelViewMatrix = GLKMatrix4Multiply(modelViewMatrix, [self modelMatrix:NO]);
    for (SGGNode * node in self.children) {
        [node renderWithModelViewMatrix:childModelViewMatrix];
    }
}

- (void)update:(float)dt {
    
    for (SGGNode * node in self.children) {
        [node update:dt];
    }
    
    GLKVector2 curMove = GLKVector2MultiplyScalar(self.moveVelocity, dt);
    self.position = GLKVector2Add(self.position, curMove);
    
}

- (GLKMatrix4) modelMatrix:(BOOL)renderingSelf {
    
    GLKMatrix4 modelMatrix = GLKMatrix4Identity;    
    modelMatrix = GLKMatrix4Translate(modelMatrix, self.position.x, self.position.y, 0);
    if (renderingSelf) {
        modelMatrix = GLKMatrix4Translate(modelMatrix, -self.contentSize.width/2, -self.contentSize.height/2, 0);
    }
    return modelMatrix;
    
}

- (CGRect)boundingBox {
    CGRect rect = CGRectMake(self.position.x, self.position.y, self.contentSize.width, self.contentSize.height);
    return rect;
}

- (void)addChild:(SGGNode *)child {
    [self.children addObject:child];
}

@end

This code is mostly moving methods from SGGSprite.h to SGGSprite.m. However, there are a few changes to point out:

  • Render method. The render method takes a paremter of the current transform so far. For example, if its parent has moved 100 points to the right, this will be a matrix representing a translation of 100 points to the right. It then updates the matrix with its own transform, and calls render on all its children.
  • Update method. The main difference here is that it calls update on all of its children.
  • modelMatrix method. This takes a parameter to see if it’s rendering itself. If it is, we use the “anchor point” to shift the sprite to the lower left to render the center of the texture at the point. Otherwise, we don’t want it to affect the matrix, so we don’t do anything with the anchor point. Note that Cocos2D does not seem to do things this way (I don’t understand why not, can someone explain)?

Now with our new SGGNode class, we can greatly simplify SGGSprite. Replace SGGSprite.h with the following:

#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>
#import "SGGNode.h"

@interface SGGSprite : SGGNode

- (id)initWithFile:(NSString *)fileName effect:(GLKBaseEffect *)effect;

@end

And replace SGGSprite.m with the following:

#import "SGGSprite.h"

typedef struct {
    CGPoint geometryVertex;
    CGPoint textureVertex;
} TexturedVertex;

typedef struct {
    TexturedVertex bl;
    TexturedVertex br;    
    TexturedVertex tl;
    TexturedVertex tr;    
} TexturedQuad;

@interface SGGSprite()

@property (strong) GLKBaseEffect * effect;
@property (assign) TexturedQuad quad;
@property (strong) GLKTextureInfo * textureInfo;

@end

@implementation SGGSprite
@synthesize effect = _effect;
@synthesize quad = _quad;
@synthesize textureInfo = _textureInfo;

- (id)initWithFile:(NSString *)fileName effect:(GLKBaseEffect *)effect {
    if ((self = [super init])) {
        self.effect = effect;
        
        NSDictionary * options = [NSDictionary dictionaryWithObjectsAndKeys:
                                  [NSNumber numberWithBool:YES],
                                  GLKTextureLoaderOriginBottomLeft, 
                                  nil];
        
        NSError * error;    
        NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
        self.textureInfo = [GLKTextureLoader textureWithContentsOfFile:path options:options error:&error];
        if (self.textureInfo == nil) {
            NSLog(@"Error loading file: %@", [error localizedDescription]);
            return nil;
        }
        
        self.contentSize = CGSizeMake(self.textureInfo.width, self.textureInfo.height);
                
        TexturedQuad newQuad;
        newQuad.bl.geometryVertex = CGPointMake(0, 0);
        newQuad.br.geometryVertex = CGPointMake(self.textureInfo.width, 0);
        newQuad.tl.geometryVertex = CGPointMake(0, self.textureInfo.height);
        newQuad.tr.geometryVertex = CGPointMake(self.textureInfo.width, self.textureInfo.height);

        newQuad.bl.textureVertex = CGPointMake(0, 0);
        newQuad.br.textureVertex = CGPointMake(1, 0);
        newQuad.tl.textureVertex = CGPointMake(0, 1);
        newQuad.tr.textureVertex = CGPointMake(1, 1);
        self.quad = newQuad;

    }
    return self;
}

- (void)renderWithModelViewMatrix:(GLKMatrix4)modelViewMatrix { 
        
    [super renderWithModelViewMatrix:modelViewMatrix];
    
    self.effect.texture2d0.name = self.textureInfo.name;
    self.effect.texture2d0.enabled = YES;
    self.effect.transform.modelviewMatrix = GLKMatrix4Multiply(modelViewMatrix, [self modelMatrix:YES]);
    
    [self.effect prepareToDraw];
       
    long offset = (long)&_quad;
       
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
        
    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, geometryVertex)));
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, textureVertex)));
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        
}

@end

Not much has changed here except we removed a lot of code.

OK, let’s try this out! Go to SGGViewController.m and replace the line that calls render in glkView:drawInRect with the following:

[sprite renderWithModelViewMatrix:GLKMatrix4Identity];

Compile and run, and your project should work as normal.

“But wait a minute,” you might ask, “why did we bother doing all that refactoring, when nothing changed!”

Well, check out something we can do now that we couldn’t have done before. Let’s say we want to give this ninja a “posse” to follow him around the map. Previously we would have had to add 3 sprites, and manually set them to move all at the same rate. Now, we can add the posse as children to the main ninja, and when we move the main ninja his posse will follow!

Try this out by adding the following code to the bottom of viewDidLoad:

self.player.moveVelocity = GLKVector2Make(25, 0); 
SGGSprite * posse1 = [[SGGSprite alloc] initWithFile:@"Player.png" effect:self.effect];
posse1.position = GLKVector2Make(-25, 50);
[self.player addChild:posse1];
SGGSprite * posse2 = [[SGGSprite alloc] initWithFile:@"Player.png" effect:self.effect];
posse2.position = GLKVector2Make(-25, -50);
[self.player addChild:posse2];

Compile and run, and now we have a ninja posse!

Node moving with Child Nodes

Contributors

Over 300 content creators. Join our team.