Tile Engine

The first step in constructing our game will be to focus on the foundation of the game world: the tile engine. The tile engine is responsible for loading the tiles that represent the level and drawing them to the screen. If the level is larger than the viewable area of the screen, we will keep track of a “camera offset” that will be used to draw the portion of the level we are interested in, as well as to draw the rest of the objects on top of the level with the same offset.

Unique Tiles

To begin, we will start with an image that represents our level (see Figure 4-1). This image was constructed out of tiles that are 32×32 pixels wide and high. The level is 15 tiles wide by 25 tiles high, or 375 tiles total. However, because it was created from a small set of tiles, we need to load only the unique tiles into memory; we need only 32 tiles (see Figure 4-2).

That’s more than 90% less memory! However, we’re also going to need to store some data that tells us which unique tile image to use when rendering the level. So, in addition to the unique tile image, we will also need an index array, such as:

1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,11,12,13,14,15,0,0,0,0,2
1,0,10,0,0,16,17,18,19,20,0,0,0,0,2
1,0,0,0,0,21,22,23,24,25,0,0,4,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,10,0,0,0,4,0,0,0,0,2
1,0,4,5,0,0,0,0,0,0,0,0,0,0,2
1,0,0,10,0,0,0,0,0,0,0,0,7,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,5,6,0,0,0,0,0,0,0,2
1,3,4,0,0,10,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,8,9,0,0,2
1,0,0,0,0,0,0,0,0,0,10,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,10,8,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,4,0,0,0,0,0,2
1,0,0,0,3,4,0,0,0,0,0,9,0,0,2
1,0,0,0,0,0,0,0,0,0,5,6,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2
1,0,0,0,0,0,0,0,0,0,0,0,0,0,2

The preceding code describes the tile indexes of a 15×25-tile map. The top-left tile uses the second frame (index “1” because indexes start at “0”). The next frame uses frame index “0,” and so on.

Level 1 in its entirety

Figure 4-1. Level 1 in its entirety

Unique tile strip for Level 1

Figure 4-2. Unique tile strip for Level 1

Drawing Tiles

As we discussed in Animation, sprites are textures that contain multiple images that should be drawn in succession to represent an animation. Each image in the sprite texture is called a frame. However, the class we presented in Chapter 3 for rendering textures, GLTexture, is not suitable for drawing frames.

Specifically, we want to consider the unique tile strip (Figure 4-2) to be like a film strip. Each frame is exactly the same width, so finding the location of a single frame in the texture is as easy as x = frame*frameWidth; where frameWidth is the same as the tile size.

The iPhone also limits us to a maximum of 1,024 for both the width and the height of the texture. That means that if our unique tiles are 32×32 and we have more than 32 tiles in either direction (because 32 * 32 = 1,024), we have to wrap them to keep our image width or height from exceeding 1,024.

Warning

The OpenGL ES implementation on the iPhone will try to automatically scale down any texture larger than 1,024 in height or width. This could cause unintended side effects for any images that are too large; if you are noticing problems with your textures, make sure they aren’t too big.

That means the calculation for the frame offset becomes:

int x = (frame * tileSize) % 1024;
int row = (( frame * tileSize ) - x) / 1024;
int y = row * tileSize;

Because of the number of mathematical operations involved in calculating this position, it is more efficient to store the results after the first calculation and reuse them, rather than recalculating the frame for each tile in every loop. Therefore, we will create a Tile class that can store the offset into a GLTexture, like so:

//Tile.h
typedef enum {
    UNWALKABLE = 1,
    WATER = 2,
    EMUTEARS = 4,
} PhysicsFlags;

@interface Tile : NSObject {
    NSString* textureName;
    CGRect frame;
    PhysicsFlags flags;
}

@property (nonatomic, copy) NSString* textureName;
@property (nonatomic) CGRect frame;

- (void) drawInRect:(CGRect)rect;
- (Tile*) initWithTexture:(NSString*)texture withFrame:(CGRect) _frame;

@end

//Tile.m
@implementation Tile

@synthesize textureName;
@synthesize frame;

- (Tile*) init {
    [super init];
    flags = 0;
    return self;
}

- (Tile*) initWithTexture:(NSString*)texture withFrame:(CGRect) _frame
{
    [self init];
    self.textureName = texture;
    self.frame = _frame;
    return self;
}

- (void) drawInRect:(CGRect)rect {
    [[g_ResManager getTexture:textureName]
                   drawInRect:rect
                   withClip:frame
                   withRotation:0];
}

- (void) dealloc {
    [super dealloc];
}

@end

Notice the PhysicsFlags enumeration. We will use this later for entity-to-world collision detection.

TileWorld Class

With the unique tile index array data, we can now write a class that will render the level to the screen using the Tile class. To do this, we will create a new class named TileWorld that a GLESGameState can use. When the GameState is initialized, it will create the TileWorld and load the level data; when the GameState is rendering, it will tell the TileWorld to draw itself before anything else.

This class needs to keep a list of tiles, along with a rectangle that describes what portion of the screen to render those tiles to. In addition, our levels may be larger than the viewable area, so we will need to keep a “camera” offset to pan our rendering to display the portion of the level we want to see:

//TileWorld.h
//global tile size.
#define TILE_SIZE 32

@class Tile;
@class Entity;

@interface TileWorld : NSObject {
    Tile*** tiles;
    CGRect view; //typically will be the same rect as the screen.  in pixels.
considered to be in opengl coordinates (0, 0 in bottom left)
    int world_width, world_height; //in tiles.
    int camera_x, camera_y; //in pixels, relative to world origin (0, 0).
                            //  view will be centered on this point.

    NSMutableArray* entities;
}

- (TileWorld*) initWithFrame:(CGRect) frame;
- (void) loadLevel:(NSString*) levelFilename
         withTiles:(NSString*)imageFilename;

- (void) draw;
- (void) setCamera:(CGPoint)position;

//utility function
- (CGPoint) worldPosition:(CGPoint)screenPosition;

- (void) addEntity:(Entity*) entity;
- (Tile*) tileAt:(CGPoint)worldPosition;
- (BOOL) walkable:(CGPoint) point;

There are a number of things to note here before we move on to the implementation. First, what is that TILE_SIZE definition? We stated earlier that we would be using 32×32 pixel tiles. This definition helps us avoid magic numbers.

A magic number is a certain numeric value that is used over and over again in code, and it is bad because you cannot tell when, for example, a value of 32 is used to represent a tile size or some other concept. It’s also bad because if you wanted to change the tile size to some other value, such as 16, you’d have to do a search and replace, being careful to replace only the instances of 32 that represent tile size. You should always use a define for values instead of magic numbers, to make your intended use clear.

The next important piece of code is the Tile*** tiles; line. This represents a dynamically allocated 2D array of Tile pointers. Because the tile engine could be used to represent a level of any width or height, we must allocate the tiles dynamically.

Finally, you will notice the NSMutableArray* entities; definition. This array will keep a list of objects we wish to draw in every frame. Because objects in front should be drawn on top of objects behind them, we need to sort those objects and draw them in order (from back to front). We also need to offset the rendering position of those objects by the same camera offset the level tiles are being drawn with.

You can view the implementation of the TileWorld class in the TileWorld.mm file.

Loading

Let’s hone in on the loadLevel: function, which accepts a text file in the following format:

[height]x[width]

[comma separated tile texture indexes]

[comma separated tile physics flags]

First, it loads the file contents into an NSString. Next, it parses out the width and height values from the first line by separating them using the componentsSeparatedByString: function of NSString to grab the characters to the left and right of the x character. Then it calls the intValue function to parse the string into an integer value for each.

Once we have the width and height of the level in tiles, we know how many Tile object pointers must be allocated. This is done in the allocateWidth: height: function.

Next, we parse through the file and create a new Tile object for each tile in the level, initializing it with a CGRect representing the frame position of the unique tile index found in the .txt file, as we discussed in Drawing Tiles.

Once all of the tiles have been created, we skip past the second blank line and start parsing the physics flags. For each tile, we read in the value and initialize the flags property of the corresponding Tile object.

Finally, we can release the original file data. Remember, since we have allocated the Tiles array and objects, we will need to deallocate them when we unload the level.

Rendering

Now that we have the Tiles initialized, we can render the level. Keep in mind that the TileWorld instance will be a member of a GLESGameState class, which will be calling the draw: function:

-(void) draw
{
    CGFloat xoff = -camera_x + view.origin.x + view.size.width/2;
    CGFloat yoff = -camera_y + view.origin.y + view.size.height/2;
    CGRect rect = CGRectMake(0, 0, TILE_SIZE, TILE_SIZE);
    for(int x=0;x<world_width;x++){
        rect.origin.x = x*TILE_SIZE + xoff;

        //optimization: don't draw offscreen tiles.
        //  Useful only when world is much larger than screen,
        //  which our emu level happens to be.
        if(rect.origin.x + rect.size.width < view.origin.x ||
           rect.origin.x > view.origin.x + view.size.width) {
            continue;
        }

        for(int y=0;y<world_height;y++){
            rect.origin.y = y*TILE_SIZE + yoff;
            if(rect.origin.y + rect.size.height < view.origin.y ||
               rect.origin.y > view.origin.y + view.size.height) {
               continue;
            }
            [tiles[x][y] drawInRect:rect];
        }
    }
    if(entities){
        [entities sortUsingSelector:@selector(depthSort:)];
        for(Entity* entity in entities){
            [entity drawAtPoint:CGPointMake(xoff, yoff)];
        }
    }
}

First we calculate the offset between the world and the viewable area of the screen. We are basically transforming between world coordinates and screen coordinates.

As an optimization, we also initialize a rectangle the size of a tile for use in the Tile object’s drawInRect: function. Rather than creating the rectangle for each tile, we can set it up outside the for loop and have to change only the x and y positions, saving us some processing power.

You may recall from Chapter 2 that another form of graphics optimization is culling. Inside the for loop, we begin by culling any columns of Tiles that wouldn’t show up in the viewable area. Then we start the inner for loop to draw the actual Tiles, first being sure to cull any individual tiles in that row that are outside the viewable area.

Now we get back to the entities array. We will discuss the Entity class in more detail later, but recall that entities need to be sorted and drawn in order from back to front so that they overlap each other correctly. Because the entities variable here is an NSMutableArray we can use the sortUsingSelector method which allows us to apply a comparison function we’ve written for this purpose.

The comparison function is expected to be called as a message on one object, with a single parameter being an object of the same type that should be compared. The return value must be NSOrderedAscending, NSOrderedSame, or NSOrderedDescending based on the result of the comparison.

A default comparison function is available for use with simple types such as NSString and NSNumber. However, because we are comparing entities, we must write a custom comparison.

Our custom function is named depthSort: and is located in the Entity.m file:

- (NSComparisonResult) depthSort:(Entity*) other {
    if (self->worldPos.y > other->worldPos.y) return NSOrderedAscending;
    if (self->worldPos.y < other->worldPos.y) return NSOrderedDescending;
    //the logical thing to do at this point is return NSOrderedSame, but that
    //causes flickering when two items are overlapping at the same y. Instead,
    //one must be drawn over the other deterministically... we use the memory
    //addresses of the entities for a tie-breaker.
    if (self < other) return NSOrderedDescending;
    return NSOrderedAscending;
}

The function compares the y values of the objects, returning NSOrderedAscending if the “other” Entity has a smaller y value. Recall that y = 0 at the top of the screen, so the smaller the y value, the farther “up” on the screen the object will be drawn. We want things closer to the bottom of the screen to be considered “close” and things at the top to be considered “far,” so returning the objects in ascending order by y coordinate will make sure that entities that are higher on the screen (farther away) will be drawn first, and entities lower on the screen (closer) will be drawn last (thereby showing on top of the others).

Camera

We mentioned that the tiles are able to be offset so that the viewable area shows the part of the level we are interested in. We call it the camera offset because it’s like a camera panning around inside the level area. In particular, we want to be able to set the camera offset to follow our main character as he walks around the level.

By calling setCamera: on our TileWorld with a target position (in world coordinates), we can calculate the offset necessary to draw the tiles so that they will be visible within the screen coordinates. If we offset a point within the tile rectangle by the negated value of that point, it will show up at the top left of the viewable area.

However, we want the main character to show up in the center of the screen, not the top left. Furthermore, if the character walks close to the left side of the world, rather than staying in the middle of the screen (thereby causing the viewable area to extend beyond the level and show empty space), we want to force the camera to stay within the bounds of the level and instead cause the character to leave the center of the screen and get close to the edge. The same condition applies for the top, bottom, and right sides of the screen. Therefore, when the intended camera position is sent to setCamera:, we do a bounds check on each side of the viewable area to make sure the camera stays put, like so:

-(void) setCamera:(CGPoint)position {
    camera_x = position.x;
    camera_y = position.y;
    if (camera_x < 0 + view.size.width/2) {
        camera_x = view.size.width/2;
    }
    if (camera_x > TILE_SIZE*world_width - view.size.width/2) {
        camera_x = TILE_SIZE*world_width - view.size.width/2;
    }
    if (camera_y < 0 + view.size.height/2) {
        camera_y = view.size.height/2;
    }
    if (camera_y > TILE_SIZE*world_height - view.size.height/2) {
        camera_y = TILE_SIZE*world_height - view.size.height/2;
    }
}

Physics

Apart from drawing the tiles and entities at their appropriate offsets, the last feature of the TileEngine is physics. Remember that each Tile in the tiles array has a physics flag that was initialized when we loaded the level file.

During the GameState update function, we will be checking entity-to-world collision by asking the TileWorld for each entity if it is colliding with any tiles that have special physics flags.

To this end, we provide the tileAt: function, which will return the Tile object located at a particular point (in world coordinates). Furthermore, as a convenience, we will also provide the walkable: function to determine whether the Tile has any special physical properties we should worry about.

Get iPhone Game Development now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.