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

This is a blog post by site administrator Ray Wenderlich, an independent software developer and gamer. There are a lot of great tutorials out there on OpenGL ES 2.0, but they usually stop after drawing a rotating cube on the screen. How to take that rotating box and turn it into a full game is […] 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.

Adding a Sprite

On to the fun part – adding a sprite to the scene!

First things first – you’ll need some artwork and other resources for the game. Go ahead and download the resources for this tutorial, and add them to your project.

To make things as easy as possible to understand, we’re going to implement a sprite class in the simplest possible way to start. We’ll come back and iterate over this class several times in the tutorial for a better implementation.

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

Open SGGSprite.h and replace it with the following:

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

@interface SGGSprite : NSObject

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

@end

This is the interface for our sprite class. As you can see, right now we’re going to start as simple as possible. We’ll allow the user to specify the image file for the sprite to display, and the GLKBaseEffect (shader) that will render it. We also define a routine that the GLKViewController will call to render the sprite.

Now switch to SGGSprite.m and delete everything in the file. We’ll add the new code next – but there’s a fair bit of it, so we’re going to do it one chunk at a time. Start by pasting this in:

#import "SGGSprite.h"

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

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

First we import our header file. Next, we start creating some structures that will store the information we neeed to render the sprite. This is the most important part to understand, so I’m going to go over this in gory detail.

The TexturedVertex is a structure that we will use to keep track of the information we need at each corner of the sprite. We create a second structure, TexturedQuad, to contain 4 TexturedVertex structures – one for each corner. Bl stands for bottom left, tr stands for top right, and so on.

The TexturedVertex structure contains two pieces of information we need for each corner: the point for where to draw it on the screen (kinda, more on this later), and the area of the texture that should be mapped to that spot.

We’re going to fill in the TexturedQuad (and its four TexturedVertex) structure with values based on the size of the sprite we’re displaying. For example, here’s how we’re going to set it up for our ninja sprite:

Setting vertex and texture coordinates in OpenGL

The geometry vertices here are the point at which we want each corner of the ninja to appear on the screen. We put his lower left corner at the bottom left of the screen (0,0). Then we don’t want to deform, stretch, or scale the ninja at all, so we have the top right corner be based on the size of the sprite itself (27x40px).

The texture vertices allow you to map what spot on the texture should map to each vertex. When you’re working with sprites, most of the time you want the bottom left corner of the texture to map to the bottom left corner of the sprite – and that’s exactly what we’re doing here.

If you’re wondering why the texture vertex values only go between 0 and 1, that’s just the way texture vertices work – a value of (0.5, 0.5) would be in the middle of a texture, for example.

Even though we’re only giving OpenGL the values at each corner of the sprite, it still knows how to render all the areas in between as well. It does this just by interpolating the values automatically. For example, the point (0,20) is halfway between the bottom left and the top left. OpenGL knows that the bottom left’s texture coordinate is (0,0) and the top left’s texture coordinate is (0,1), so it can choose the value halfway inbetween (0, 0.5) for the texture coordinate.

This is just defining the data structure that will hold the geometry and texture vertex values – we haven’t actually set it up yet. It’s coming in a bit. For now, add this next:

@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;

Here we create a private category of SGGSprite. This is a fancy way of creating private variables in Objective-C. Since we define them here, inside the implementation file (.m), nobody outside our class knows about these. This makes for better encapsulation, and is a general good practice.

We define and synthesize three private properties here:

  • GLKBaseEffect * effect: The effect (shader) that we’ll use to render the sprite. More on this later.
  • TexturedQuad quad: The instance of our TexturedQuad structure. We’ll fill this in as described above soon.
  • GLKTextureInfo * textureInfo: We’re going to use GLKit’s GLKTextureLoader class to easily load our texture. It will return some info about the texture, which we’ll store here.

Continue on by adding this next:

- (id)initWithFile:(NSString *)fileName effect:(GLKBaseEffect *)effect {
    if ((self = [super init])) {  
        // 1
        self.effect = effect;
        
        // 2
        NSDictionary * options = [NSDictionary dictionaryWithObjectsAndKeys:
                                  [NSNumber numberWithBool:YES],
                                  GLKTextureLoaderOriginBottomLeft, 
                                  nil];
        
        // 3
        NSError * error;    
        NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
        // 4
        self.textureInfo = [GLKTextureLoader textureWithContentsOfFile:path options:options error:&error];
        if (self.textureInfo == nil) {
            NSLog(@"Error loading file: %@", [error localizedDescription]);
            return nil;
        }
        
        // TODO: Set up Textured Quad

    }
    return self;
}

Here we create the initializer for our class. Let’s go over this line by line:

  1. Stores the GLKBaseEffect that will be used to render the sprite.
  2. Sets up the options so that when we load the texture, the origin of the texture will be considered the bottom left. If you don’t do this, the origin will be the top left (which we don’t want, because it won’t match OpenGL’s coordinate system).
  3. Gets the path to the file we’re going to load. The filename is passed in. Note that if you pass nil as the type, it will allow you to enter the full filename in the first parameter. Believe it or not, I just learned this after 2 years of iOS dev :P
  4. Finally loads the texture with the handy GLKTextureLoader class. You should appreciate this – it used to take tons of code to accomplish this :]

Next add this after the TODO:

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;

This fills in our quad as described in the diagram above. I’ll post the diagram again here for handy reference.

Setting vertex and texture coordinates in OpenGL

Note that the texture vertex values will be the same no matter what sprite you use, since we always want to map the texture the same way with respect to the various corners. The geometry vertex values will change though, based on the size of the texture.

OK, one final bit to add for our sprite class:

- (void)render { 
            
    // 1
    self.effect.texture2d0.name = self.textureInfo.name;
    self.effect.texture2d0.enabled = YES;

    // 2    
    [self.effect prepareToDraw];
              
    // 3
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);

    // 4
    long offset = (long)&_quad;        
    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)));

    // 5    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        
}

@end

Let’s go over this line by line as well. Note that to fully understand this (especially the pointer math), you need to have some basic familiarity with C. If some of this confuses you, ask in the forums and somebody can help you out.

  1. When you use a GLKBaseEffect to render geometry, you can specify a texture to use for the drawing. You do this by setting the texture2d0.name to the textureInfo.name, and setting texture2d0.enabled to YES. There’s also a second texture unit for more advanced effects – more details are in iOS 5 by Tutorials.
  2. Before you draw anything, you have to call prepareToDraw on the GLKBaseEffect. Note that you should only call this after you have finished configuring the parameters on the GLKBaseEffect the way you want.
  3. There are two pieces of information we want to pass to the effect/shader: the position and texture coordinate of each vertex. We use this function to enable us to pass the values through.
  4. Next we need to actually send each piece of data. We do that via the glVertexAttribPointer method. For example, the first function says “I want to send some position values. I’m going to send 2 floats over. After you read the first two floats, advance the size of the TexturedVertex structure to find the next two. And here’s a pointer to where you can find the first vertex.”
  5. Finally, we draw the geometry, specifying 4 vertices of data drawn as a triangle strip. This warrants some special discussion, continued below.

We’ve arranged the vertices in our array in a special manner so they work nicely with the triangle strip drawing method. This prevents us from having to define a second array to specify indices.

Here’s a diagram of how the triangle strip method draws our quad:

Sprite drawn as an OpenGL triangle strip

The way a triangle strip works is the following:

  • The first three points define the first triangle.
  • The next point defines the next triangle, along with the previous two points.
  • Usually this algorithm continues on and on, but since we only have 4 vertices it actually stops here for us.

Note: Here we are passing data directly to OpenGL without using vertex buffers or vertex array objects. A while ago I thought using vertex buffers and vertex array objects would increase performance, but after some testing it seems they don’t really. After some investigation I found this great blog post by Daniel Pasco that explains the subject in more detail. Long story short: this simple way works fine.

OK! Time to try this code out. Open SGGViewController.m and make the following changes:

// Add to top of file
#import "SGGSprite.h"

// Add inside private interface
@property (strong) GLKBaseEffect * effect;
@property (strong) SGGSprite * player;

// Add in synthesize section
@synthesize player = _player;

// Add at bottom of viewDidLoad
self.effect = [[GLKBaseEffect alloc] init];

GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, 480, 0, 320, -1024, 1024);
self.effect.transform.projectionMatrix = projectionMatrix;

self.player = [[SGGSprite alloc] initWithFile:@"Player.png" effect:self.effect];    

// Replace glkView:drawInRect with the following
- (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.player render];
}

First we include the header file and create and synthesize a property for our player sprite.

Next we set the projection matrix on the transform, which controls how things are rendered to the screen.

There are two main types of projection matrices: perspective projections (makes things appear smaller the farther away they are) and orthographic matrices (makes things the same size regardless of how far away they are). Usually for 2D games you want an ortohographic matrix.

The good news is with GLKit you don’t really need to understand how the math works to create one. You just specify the left side of the screen, right side of the screen, bottom, and top as parameters to GLKMatrix4MakeOrtho. The last two parameters are the minimum and maximum z values. Our z values are going to default to 0 so it doesn’t really matter, but I chose a range here.

Finally we create a player sprite, passing in the effect, and call render in glkView:drawInRect. We also include two lines to set up blending properly, so that transparency in the texture shows up properly.

Note: If you want more info about how the blending modes work, check out this tutorial.

One last step. We don’t want the status bar to appear in our game, so open Supporting Files\SimpleGLKitGame-Info.plist and create a new entry for “Status bar is initially hidden”, and set it to YES.

Setting status bar to hidden

That’s it – compile and run, and you’ll see the ninja appear on the screen!

Drawing a ninja sprite on the screen with OpenGL

Contributors

Over 300 content creators. Join our team.