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 3 of 4 of this article. Click here to view the first page.

More About Nodes

Right now you can set the position of a node, but there are two other attributes on a node you commonly want to set: rotation and scale.

We won’t actually be using either of these in our game, but I wanted to show you guys how to do this because most likely you will need this in your games.

Make the following changes to SGGNode.h:

@property (assign) float rotation;
@property (assign) float scale;
@property (assign) float rotationVelocity;
@property (assign) float scaleVelocity;

Here we add a property for the rotation (in degrees) and the scale (1.0 = normal size). We also add a rotationVelocity and scaleVelocity for a easy way to get these values to change over time.

Next switch to SGGNode.m and make the following changes:

// Add in @synthesize section
@synthesize rotation = _rotation;
@synthesize scale = _scale;
@synthesize rotationVelocity = _rotationVelocity;
@synthesize scaleVelocity = _scaleVelocity;

// Add in init
self.scale = 1;

// Add at bottom of update
float curRotate = self.rotationVelocity * dt;
self.rotation = self.rotation + curRotate;

float curScale = self.scaleVelocity * dt;
self.scale = self.scale + curScale;

// Add at bottom of modelMatrix: method
float radians = GLKMathDegreesToRadians(self.rotation);
modelMatrix = GLKMatrix4Rotate(modelMatrix, radians, 0, 0, 1);

modelMatrix = GLKMatrix4Scale(modelMatrix, self.scale, self.scale, 0);

The key part here is in the update method – we use the GLKMath methods to update our matrix based on the rotation and scale values.

Let’s try this out. Switch to SGGVIiewController.m and add these lines to the bottom of viewDidLoad:

self.player.rotationVelocity = 45;
self.player.scaleVelocity = 0.1;

Now you have a rotating and scaling posse!

Node rotating and scaling with child nodes

However we don’t want a posse at all, so go ahead and comment out the lines that create the posse :]

Also, there’s a problem we have to fix. The bounding box method currently doesn’t take into effect the scale or the rotation of an item. This will mess up our collision detection if we were to scale a monster to be 3x the size, for example.

Try it out for yourself. Add the following to the end of addTarget:

target.scale = 3.0;

Compile and run, and shoot at a monster but aim for the very bottom or very top. You’ll notice that it sometimes doesn’t register the collision, because it’s using the monster’s original size for the bounding box.

To fix this, update the boundingBox method in SGGNode.m to the following:

- (CGRect)boundingBox {    
    CGRect rect = CGRectMake(0, 0, self.contentSize.width, self.contentSize.height);
    GLKMatrix4 modelMatrix = [self modelMatrix:YES];
    CGAffineTransform transform = CGAffineTransformMake(modelMatrix.m00, modelMatrix.m01, modelMatrix.m10, modelMatrix.m11, modelMatrix.m30, modelMatrix.m31);    
    return CGRectApplyAffineTransform(rect, transform);    
}

Here we start with the non-translated rectangle for the node, and then get the model view matrix. We then need to convert our 4×4 matrix into a CGAffineTransform. Don’t worry if you don’t understand what line 3 does, just know that it converts a 4×4 matrix to a CGAffineTransform.

We then use a handy built-in method to apply a CGAffineTransform to a rectangle, and give us the closest matching bounding rectangle.

Compile and run, and now you should be able to shoot the giant monsters just fine!

Shooting giant monsters

When you’re done, comment out the line that sets them to the larger size as we won’t be needing that.

View Controller to Scene

Now that we have our node class in place, we can start moving the game logic that is in SGGViewController.m into a scene class.

First things first. We’re going to add a stub method onto SGGNode to handle receiving taps (we’ll need this later on), so add the following method declaration to SGGNode.h:

- (void)handleTap:(CGPoint)touchLocation;

And the following stub declaration to SGGNode.m:

- (void)handleTap:(CGPoint)touchLocation {   
}

You could modify this to forward touches to children based on their bounding boxes, etc. but again to keep thing simple we’re just going to implement what we need here – which is the scene getting info about when there’s a touch.

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

Open SGGActionScene.h and replace it with the following:

#import "SGGNode.h"

@interface SGGActionScene : SGGNode

- (id)initWithEffect:(GLKBaseEffect *)effect;

@end

Then open SGGActionScene.m and replace it with the following:

#import "SGGActionScene.h"
#import "SGGSprite.h"
#import "SimpleAudioEngine.h"

@interface SGGActionScene ()
@property (strong) GLKBaseEffect * effect;
@property (strong) SGGSprite * player;
@property (assign) float timeSinceLastSpawn;
@property (strong) NSMutableArray *projectiles;
@property (strong) NSMutableArray *targets;
@property (assign) int targetsDestroyed;
@end

@implementation SGGActionScene
@synthesize effect = _effect;
@synthesize player = _player;
@synthesize timeSinceLastSpawn = _timeSinceLastSpawn;
@synthesize projectiles = _projectiles;
@synthesize targets = _targets;
@synthesize targetsDestroyed = _targetsDestroyed;

- (id)initWithEffect:(GLKBaseEffect *)effect {
    if ((self = [super init])) {
        self.effect = effect;
        
        self.player = [[SGGSprite alloc] initWithFile:@"Player.png" effect:self.effect];    
        self.player.position = GLKVector2Make(self.player.contentSize.width/2, 160);
        
        [self.children addObject:self.player];    
                       
        self.projectiles = [NSMutableArray array];
        self.targets = [NSMutableArray array];
        
        [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"];
        [[SimpleAudioEngine sharedEngine] preloadEffect:@"pew-pew.wav"];
                        
    }
    return self;
}

- (void)handleTap:(CGPoint)touchLocation {
    
    // 2
    GLKVector2 target = GLKVector2Make(touchLocation.x, touchLocation.y);
    GLKVector2 offset = GLKVector2Subtract(target, self.player.position);
    
    // 3
    GLKVector2 normalizedOffset = GLKVector2Normalize(offset);
    
    // 4
    static float POINTS_PER_SECOND = 480;  
    GLKVector2 moveVelocity = GLKVector2MultiplyScalar(normalizedOffset, POINTS_PER_SECOND);
    
    // 5
    SGGSprite * sprite = [[SGGSprite alloc] initWithFile:@"Projectile.png" effect:self.effect];
    sprite.position = self.player.position;
    sprite.moveVelocity = moveVelocity;
    [self.children addObject:sprite];
    
    [self.projectiles addObject:sprite];
    
    [[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew.wav"];
}

- (void)addTarget {
    SGGSprite * target = [[SGGSprite alloc] initWithFile:@"Target.png" effect:self.effect];
    [self.children addObject:target];
    
    int minY = target.contentSize.height/2;
    int maxY = 320 - target.contentSize.height/2;
    int rangeY = maxY - minY;
    int actualY = (arc4random() % rangeY) + minY;
    
    target.position = GLKVector2Make(480 + (target.contentSize.width/2), actualY);    
    
    int minVelocity = 480.0/4.0;
    int maxVelocity = 480.0/2.0;
    int rangeVelocity = maxVelocity - minVelocity;
    int actualVelocity = (arc4random() % rangeVelocity) + minVelocity;
    
    target.moveVelocity = GLKVector2Make(-actualVelocity, 0);
    
    [self.targets addObject:target];

}

- (void)update:(float)dt {
    
    [super update:dt];
    
    NSMutableArray * projectilesToDelete = [NSMutableArray array];
    for (SGGSprite * projectile in self.projectiles) {
        
        NSMutableArray * targetsToDelete = [NSMutableArray array];
        for (SGGSprite * target in self.targets) {            
            if (CGRectIntersectsRect(projectile.boundingBox, target.boundingBox)) {
                [targetsToDelete addObject:target];
            }            
        }
        
        for (SGGSprite * target in targetsToDelete) {
            [self.targets removeObject:target];
            [self.children removeObject:target];
            _targetsDestroyed++;
        }
        
        if (targetsToDelete.count > 0) {
            [projectilesToDelete addObject:projectile];
        }
    }
    
    for (SGGSprite * projectile in projectilesToDelete) {
        [self.projectiles removeObject:projectile];
        [self.children removeObject:projectile];
    }
    
    self.timeSinceLastSpawn += dt;
    if (self.timeSinceLastSpawn > 1.0) {
        self.timeSinceLastSpawn = 0;
        [self addTarget];
    }        
}

@end

Wow – a lotta code here, but it is literally ripped out from SGGViewController.m, so it’s all stuff we covered before. You can just copy/paste it in there.

Now we can greatly simplify SGGViewController.m. Open it up and replace it with the following:

#import "SGGViewController.h"
#import "SGGActionScene.h"

@interface SGGViewController ()
@property (strong, nonatomic) EAGLContext *context;
@property (strong) GLKBaseEffect * effect;
@property (strong) SGGNode * scene;
@end

@implementation SGGViewController
@synthesize effect = _effect;
@synthesize context = _context;
@synthesize scene = _scene;

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    
    if (!self.context) {
        NSLog(@"Failed to create ES context");
    }
    
    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    [EAGLContext setCurrentContext:self.context];
    
    self.effect = [[GLKBaseEffect alloc] init];
    
    GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, 480, 0, 320, -1024, 1024);
    self.effect.transform.projectionMatrix = projectionMatrix;
    
    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)];                                                               
    [self.view addGestureRecognizer:tapRecognizer];
    
    self.scene = [[SGGActionScene alloc] initWithEffect:self.effect];
    
}

- (void)handleTapFrom:(UITapGestureRecognizer *)recognizer { 
        
    // 1
    CGPoint touchLocation = [recognizer locationInView:recognizer.view];
    touchLocation = CGPointMake(touchLocation.x, 320 - touchLocation.y);
    
    [self.scene handleTap:touchLocation];
    
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

#pragma mark - GLKViewDelegate

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {    
    glClearColor(1, 1, 1, 1);
    glClear(GL_COLOR_BUFFER_BIT);    
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
    
    [self.scene renderWithModelViewMatrix:GLKMatrix4Identity];
}

- (void)update {    
    
    [self.scene update:self.timeSinceLastUpdate];
}
 
@end

Ah, much cleaner, eh? Compile and run, and the game should work just as before – but nicely abstracted into a scene class.

Congratulations, the refactoring is complete! Now we can finally move on to creating a win/lose scene :]

Contributors

Over 300 content creators. Join our team.