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 […]

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 2.0 and GLKit project, created a class for Sprites, rendered them to the scene, and made them move.

In this second and final part of the series, we’ll let the ninja shoot stars, kill monsters, and win the game!

Keep reading to become a code ninja and finish this game!

Shooting Projectiles

We’re going to implement shooting by registering a tap gesture recognizer on the view. When we receive a tap, we’ll perform the following steps:

Convert the touch to OpenGL coordinates. Figure out the direction (vector) of the touch, with respect to the ninja. Normalize the vector so that it is a length of 1. This makes it easy to make a vector pointing in the same direction as the offset vector, but with a particular length, which we’ll need in the next step. Multiply the normalized offset by the velocity we want the ninja star to move. We now have a vector pointing in the same direction as the diference between the touch and the ninja, but a length based on where we want the ninja star to be after 1 second. Create a ninja star sprite and set its velocity based on the above.

Here’s a diagram of the above:

Now let’s see this in code. Make the following changes to SGGViewController.m:

// Add to bottom of viewDidLoad UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapFrom:)]; [self.view addGestureRecognizer:tapRecognizer]; // Add new method to file (anywhere) - (void)handleTapFrom:(UITapGestureRecognizer *)recognizer { // 1 CGPoint touchLocation = [recognizer locationInView:recognizer.view]; touchLocation = CGPointMake(touchLocation.x, 320 - touchLocation.y); // 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]; }

The numbered steps here correspond to the same steps described above – flip back if you’re confused.

Compile and run, and now you should be able to tap to shoot ninja stars!

Collision Detection

So now we have shurikens flying everywhere – but what our ninja really wants to do is to lay some smack down. So let’s add in some code to detect when our projectiles intersect our targets.

First, we need to add a new method to SGGSprite in order to return the bounding box for the sprite on the screen. We need this so we can check if two sprites collide.

Add the following to SGGSprite.h:

- (CGRect)boundingBox;

And then add the implementation to SGGSprite.m:

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

The next thing we have to do is keep better track of the targets and projectiles currently in the scene. Make the following changes to SGGViewController.m:

// Add to private @interface @property (strong) NSMutableArray *projectiles; @property (strong) NSMutableArray *targets; @property (assign) int targetsDestroyed; // Add to synthesize section @synthesize projectiles = _projectiles; @synthesize targets = _targets; @synthesize targetsDestroyed = _targetsDestroyed; // Add to bottom of viewDidLoad self.projectiles = [NSMutableArray array]; self.targets = [NSMutableArray array]; // Add to bottom of handleTapFrom [self.projectiles addObject:sprite]; // Add to bottom of addTarget [self.targets addObject:target];

Now we have an array of the targets and projectiles currently in the scene. We also added a variable to keep track of the number of targets destroyed (we’ll need this later).

Finally, add the meat of the logic to the beginning of the update method:

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

The above should be pretty clear. We just iterate through our projectiles and targets, creating rectangles corresponding to their bounding boxes, and use CGRectIntersectsRect to check for intersections. If any are found, we remove them from the scene and from the arrays. Note that we have to add the objects to a “toDelete” array because you can’t remove an object from an array while you are iterating through it. Again, there are more optimal ways to implement this kind of thing, but I am going for the simple approach.

Compile and run, and now when your projectiles intersect targets they should disappear!

Gratuitous Music and Sound Effects

If you’ve been following the tutorials on this site, you’ll know that we can never leave you hanging without some awesome music and sound effects for our game tutorials :]

You should already have some cool background music I made (background-music-aac.caf) and my awesome pew-pew sound effect (pew-pew.wav) in your project, from the resources for this tutorial that you added to the project in part 1.

We’re going to cheat a little bit in this part of the tutorial and use a premade sound engine, CocosDenshion. CocosDenshion is actually the sound engine that comes with Cocos2D, but you can use it in any project just by including the files.

We could play the music and effects ourselves by using the easy-to-use AVAudioPlayer, but you’ll find that performance is not up-to-snuff for playing rapid sound effects like we want for this game. Alternatively you could use OpenAL yourself, but the focus of this tutorial is not on OpenAL so we are going to go the easy route.

So go ahead and add the CocosDenshion files to your project. You can copy them from your Cocos2D installation, any Cocos2D project, or just be lazy and download a CocosDenshion zip I made.

If you try to compile your project you’ll get a ton of errors, because CocosDenshion is not ARC compatible. Luckily fixing this is easy. Select your project in the Project Navigator, select the SimpleGLKitGame target, and go to the Build Phases tab.

Double click CDAudioManager.m, and a popup will appear. Enter -fno-objc-arc into the box – this makes them compile without ARC support.

Sadly you can only do one file at a time, so repeat this process for all 4 of the CocosDenshion files:

CDAudioManager.m

CDOpenALSupport.m

CocosDenshion.m

SimpleAudioEngine.m

CocosDension also requires some libraries. Sitll in the Build Phases tab, expand the Link Binary With Libraries section and add the following libraries:

AudioToolbox.framework

OpenAL.framework

AVFoundation.framework

Your project should build OK now. Phew – hard part is done – now let’s play some music! Make the following changes to SGGViewController.m:

// Add to top of file #import "SimpleAudioEngine.h" // Add to end of viewDidLoad [[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"]; [[SimpleAudioEngine sharedEngine] preloadEffect:@"pew-pew.wav"]; // Add to end of handleTapFrom [[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew.wav"];

Compile and run, and enjoy the groovy tunes!

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!

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!

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!

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 :]

Creating a Win/Lose Scene

To allow replacing the scene, we’re going to make the scene property on SGGViewController public. Then other classes can get a reference to the SGGViewController and replace it directly. There are better ways to do this, but again – keeping it simple ;]

So go to SGGViewController.h and add the following property:

@property (strong) SGGNode * scene;

And switch to SGGViewController.m and comment it out in the private interface:

//@property (strong) SGGNode * scene;

OK cool – now let’s create our Game Overs scene. Create a new file with the iOS\Cocoa Touch\Objective-C class template. Enter SGGGameOverScene for the Class, SGGNode for the Subclass, click Next, and click Create.

Open SGGGameOverScene.h and replace it with the following:

#import "SGGNode.h" @interface SGGGameOverScene : SGGNode - (id)initWithEffect:(GLKBaseEffect *)effect win:(BOOL)win; @end

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

#import "SGGGameOverScene.h" #import "SGGSprite.h" #import "SGGViewController.h" #import "SGGActionScene.h" #import "SGGAppDelegate.h" @interface SGGGameOverScene () @property (assign) float timeSinceInit; @property (strong) GLKBaseEffect * effect; @end @implementation SGGGameOverScene @synthesize timeSinceInit = _timeSinceInit; @synthesize effect = _effect; - (id)initWithEffect:(GLKBaseEffect *)effect win:(BOOL)win { if ((self = [super init])) { self.effect = effect; if (win) { SGGSprite * winSprite = [[SGGSprite alloc] initWithFile:@"YouWin.png" effect:effect]; winSprite.position = GLKVector2Make(240, 160); [self addChild:winSprite]; } else { SGGSprite * loseSprite = [[SGGSprite alloc] initWithFile:@"YouLose.png" effect:effect]; loseSprite.position = GLKVector2Make(240, 160); [self addChild:loseSprite]; } } return self; } - (void)update:(float)dt { self.timeSinceInit += dt; if (self.timeSinceInit > 3.0) { SGGActionScene * scene = [[SGGActionScene alloc] initWithEffect:self.effect]; SGGAppDelegate * delegate = [[UIApplication sharedApplication] delegate]; UIWindow * mainWindow = [delegate window]; SGGViewController * viewController = (SGGViewController *) mainWindow.rootViewController; viewController.scene = scene; } } @end

This simply places a premade image of “You Win” or “You Lose” in the middle of the screen on startup. After a certain amount of time elapses, it creates a new ActionScene and sets the currently running scene to this new scene.

Only thing left to do is run the GameOverScene on win or lose! Open up SGGActionScene.m and make the following changes:

// Add to top of file #import "SGGGameOverScene.h" #import "SGGAppDelegate.h" #import "SGGViewController.h" // Add new method, before update method - (void)gameOver:(BOOL)win { SGGGameOverScene * gameOver = [[SGGGameOverScene alloc] initWithEffect:self.effect win:win]; SGGAppDelegate * delegate = [[UIApplication sharedApplication] delegate]; UIWindow * mainWindow = [delegate window]; SGGViewController * viewController = (SGGViewController *) mainWindow.rootViewController; viewController.scene = gameOver; } // Add at bottom of update method if (_targetsDestroyed > 5) { [self gameOver:YES]; return; } BOOL lose = NO; for (int i = self.children.count - 1; i >= 0; i--) { SGGSprite * sprite = [self.children objectAtIndex:i]; if (sprite.position.x <= -sprite.contentSize.width/2 || sprite.position.x > 480 + sprite.contentSize.width/2 || sprite.position.y <= -sprite.contentSize.height/2 || sprite.position.y >= 320 + sprite.contentSize.height/2) { if ([self.targets containsObject:sprite]) { [self.targets removeObject:sprite]; [self.children removeObjectAtIndex:i]; lose = YES; } else if ([self.projectiles containsObject:sprite]) { [self.projectiles removeObject:sprite]; [self.children removeObjectAtIndex:i]; } } } if (lose) { [self gameOver:NO]; }

Here we add the game logic. If the player destroys more than 5 targets, they win, so create and display a game over scene.

We also add some cleanup code to remove sprites that are outside the bounds of the scene to avoid endless memory allocations. If a monster escapes the boundary of the screen, the game is over.

Guess what – you’re done! Compile and run, and see if you can beat the game!

Where To Go From Here?

Here is the finished and refactored project from this tutorial.

Congratulations, you have made a complete game with OpenGL ES 2.0 and GLKit! You now have experience with creating a very simple game engine, and if you like you could use this as the basis for your own game engine.

This tutorial should also have given you a better appreciation for Cocos2D and all that it does for you :] Since this engine is organized somewhat similarly to Cocos2D, you should also better understand how it works.

If you have any questions or comments on OpenGL ES 2.0, GLKit, or this tutorial, please join the forum discussion below!



This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer.