O'Reilly logo

iPhone Game Development by Paul Zirkle, Joe Hogue

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. 2D Game Engine

Finally, the good stuff! The first three chapters of this book introduced the basics involved in programming for the iPhone and the concepts behind a game engine. We even laid out a nice framework to get us started. All that’s left is to make our game…but wait, aren’t we forgetting something? What kind of game are we going to make? Before we start coding away, we need to have a plan. We need a game design.

Game Design

The first step in coming up with a good game design is brainstorming. What will the game look like, and how will it feel to play the game? What are the goals, and how will you keep players entertained? Remember that as much fun as it is to make a game, the end result needs to be something that’s fun to play.

To begin, we’ll need to keep in mind the special considerations of the iPhone:

  • Touch-screen input

  • Limited resolution and memory

  • Play sessions that are short and can be interrupted at any time by a phone call

This means we need to come up with a design that incorporates:

  • A simplistic input scheme

  • Efficient use of memory for graphics and sound

  • Game play that is broken into short segments that can easily be stopped and resumed

To fulfill all of these requirements, we will create a 2D tile-based engine. Tile-based means the level background will be composed of a limited number of square tiles that are repeated and put together to form the level (this allows us to create a large level that uses only a small amount of texture memory). Most 2D games are static (the screen does not scroll at all—think puzzle games), side-scrolling (think Super Mario Bros.), or top-down (like Zelda). A puzzle game is going to be too limiting in terms of AI logic, and most side-scrolling games are too demanding in terms of user input, so top-down best fits our needs. And since this is an O’Reilly book, we’ll borrow from the theme of O’Reilly animals.

With these parameters in mind, let’s create O’Reilly’s Wildlife Adventure. As the name suggests, this will be an adventure game involving wild animals. Players will need to help our main character, Tom O’Reilly, as he tends to the animals of a wild animal park. Each “level” will represent a different animal pen, which will nicely segment game play and allow us to challenge players in unique ways with each type of animal they encounter. Tom will handle various special objects as he deals with each animal.

This game design probably won’t win any major awards, but it will definitely cover all of the major facets of game development for the iPhone.

Feature List

Now that we have finished brainstorming, it’s time to nail down exactly what we need to bring this design into reality.

Graphical layout

To render the game, we will need to draw:

  • The level background

  • The main character

  • Several different types of animals

  • Any random objects that may appear in a level

If the levels are larger than the screen, we will need to be able to scroll our view into the world to keep the player centered on the screen.

Input

We will need to interpret user input to:

  • Control the main character

  • Create an AI engine to control the animal behaviors

  • Code up some physics to handle the movement and interaction of those entities with each other and the level

We will also need game logic to determine when goals have been met and store the player’s progress.

Display

Apart from drawing the world, we may need to overlay additional information in a heads-up display (HUD) for things such as:

  • The player’s condition

  • Notification of any special items that have been picked up

  • Possibly a button to open a pause menu

Furthermore, we will probably need to display some text for dialogs if our main character has anything interesting to say.

Game progress

So far, we’ve discussed only what goes on during game play. We will also need to add:

  • Code to link the levels together and show game progression

  • Something like a map that lets players choose what level to play next and unlocks new areas as they complete old ones

Putting it all together, we have the following technical feature list:

  • A 2D tile-based render engine with scrolling capability

  • Animated sprite entities for the main character, animals, and possibly level entities

  • Functions to handle user input to control the main character

  • Animal entity logic for several types of animals (such as lions, crocodiles, and emus)

  • Physics to allow entities to move where they should, and not where they shouldn’t (such as into other entities)

  • Level logic to determine when goals have been met and to store the player’s progress through the game

  • A HUD for important information, including character dialogs

  • A menu that allows players to select what level to play and displays their progression through the game

User Input Scheme

As we mentioned earlier, the touch-screen feature of the iPhone means user input has to be a major focus of game design. Specifically, there are three problems that our game design should try to avoid.

The first problem to avoid is requiring accurate touches. When a player touches the screen, she is actually touching many pixels at once; the iPhone will estimate the center pixel among those pixels and events will correspond to just that one pixel. It is important to make touchable interfaces (such as buttons) or in-game objects large enough that an inaccurate touch event will still find its mark if the player touched close enough. Therefore, we should use a rule of thumb (pun intended) that touchable objects should be at least 44×44 pixels.

The second problem to avoid is causing the player to cover the screen with her hand while trying to use the screen for input. This is one of the major downsides to a touch-screen interface; because both input and output happen on the same surface, they end up fighting each other for space. One way to solve this is to require as little input from the user as possible. Another is to take user input only in nonessential parts of the screen, or nonessential application states—states where fast response to game actions is not required.

The third problem to avoid is requiring many different types of touches. The iPhone can record not only single and double taps but also finger dragging. With a little work, it is even possible to detect when the player forms symbols (such as a circle or square) with her finger. However, it is important to keep in mind that game play can get frantic, and complex input schemes will frustrate players if they cannot quickly and easily communicate their intended actions to the game.

We chose a top-down design to help reduce the amount of user input activity needed to play the game. Specifically, with a top-down view, we can keep the camera zoomed out far enough that the player can tap an area of the level she wants to move to, and the game will plot out a path and move the character for her. In this way, one simple input (tapping a part of the level) is translated into a series of more complex movements (move forward, turn right, move forward, turn left, move forward), saving the player from a lot of busy work.

However, movement is not the only type of input that will be required during game play. We may also need to handle special actions such as jumping, picking up items, and anything else the game requires. We can handle these in a similar fashion to movement: by translating a simple command into an intelligent behavior. Specifically, we know that if the player touches an area of the level the character is not standing in, we should move the character to that point. However, if she taps the square the character is already standing in, we can treat it as a special command.

But how do we know what that special command is? By determining the context in which the command is used. If the character is standing on the edge of a gap when the player taps, we could consider this as a command to jump across the gap. If instead the character is standing next to a door, we could consider this as a command to open the door. Based on the proximity of the character to a special feature of the level, tapping the character will cause the character to interact with that feature. Such actions are considered context-sensitive.

This allows us to have a system that can produce a lot of complex behavior, but the input system remains absolutely simple. In fact, the player needs to know only two motions: tap the area you want the character to move to, or tap the character itself to perform a context-sensitive action.

Learning Curve

By now, we have a basic understanding of the structure of the game world (top-down, tile-based, with entities moving about) and how the user will interact with it (by tapping the level or the character). It is time to come up with some level concepts. However, an important part of level design is to consider the game’s learning curve.

When a player first picks up your game, she will not be an expert at it. In fact, learning how to play your game will be part of the fun for her. But if the first levels she encounters are too challenging, she will become frustrated easily and stop playing. On the other hand, if she masters your game in the first five minutes, she will get bored and put it down.

Therefore, it is important to design the levels in such a fashion that the player is neither bored nor overwhelmed. Typically, you should try to introduce one new concept (or a combination of two previously introduced concepts) per level to ensure that players stay awake.

Level 1

Our first level will involve merely walking about, with no context-sensitive actions or fear of death. However, if the player only had to walk from one side of the level to the other without a challenge, she would get bored. So, we will give her something to chase to motivate her to keep playing.

A mother emu’s chicks have wandered from the nest and our character will have to chase them in the right direction. This will also give us a chance to work on some flocking AI.

Level 2

Our second level will continue with the movement-only-centered activity, but will increase the intensity of the game.

Specifically, we will ask the player to walk across a habitat filled with sleeping lions to the back of the cave to grab a McGuffin. A McGuffin is a nonsense term that means “something that moves the plot.”[1] Basically, we need a reason to send our main character to the back of a lion’s cave. It doesn’t have to be a good reason, just a reason. That’s what the McGuffin is for. If the player avoids the lions, the level will be easy. If she gets too close, the lions will wake up and swipe at her. This will allow us to work on some multistate AI (sleeping and chasing).

Level 3

The third level will dial back the intensity, but it will introduce a new concept. Here, we’ll teach the player how to use the context-sensitive actions by providing her with button objects she can interact with to change the level.

We will set up the level as a puzzle: a grid of cells (3×3) will contain a cat and a mouse, as well as cheese below a control room that contains three buttons and the main character. Between each cell is a door that can only be opened by pressing the corresponding button from the control room.

The goal is to open the doors in such a way that the mouse reaches the cheese without meeting the cat. This will show the flexibility of our game engine, being able to create a casual puzzle type of game play experience.

Level 4

The fourth level will focus on using the context-sensitive action method of input in a time-sensitive setting. We will place a McGuffin at the top end of the level past several streams of water that must be crossed.

To do this, we will place logs in the streams that the player can jump on, using the context-sensitive action. The player will have to move quickly, however, as the logs will slowly sink and disappear into the water, requiring a series of decisive jumps.

And if that weren’t enough, we will also have crocodiles patrolling the waters, jumping and snapping at the player if she gets too close.

At this point, we are ready to start implementing. Please download the Chapter 4 example code from https://sourceforge.net/projects/iphonegamebook/files/.

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.

Animation

Our game will contain a main character and several animated animals. Each of these entities will require one or more animations for each of their behaviors and each of the directions they could be facing during those behaviors. For instance, the main character could have “idle,” “walking,” and “jumping” animations in the “north,” “south,” “east,” and “west” directions.

Once our artist has rendered each of these, we can assemble them together into a sprite texture similar to the unique tile strip texture we will use for the tile engine map. However, these animations are much more complex than the unique tile strip. See Figure 4-3, for instance, for the images we need to represent a walking emu chick.

Emu chick walking animation
Figure 4-3. Emu chick walking animation

As you can see, the emu chick sprite texture has three animations: walking west, walking north, and walking south (we omitted walking east because it can be created by flipping the walking west animation horizontally, which saves precious texture memory).

To make things more complicated, the animations are set up in such a way that some frames should be used multiple times in a single animation sequence. Specifically, the walking west animation should be displayed in the frame sequence 0, 1, 2, 3, 4. If we had a more complex animation, this format would support something similar to 0, 1, 0, 2, 0, 3…, where the frame indexes are not sequential.

Additionally, we could want some frames to take longer than others, so we should specify how long (in milliseconds) each frame should take: 200, 300, 200, 100, 200, 100, 100, 200. This adds up to an animation sequence that takes 1,400 ms (or 1.4 s) to complete.

To complicate things further, some animations should be looped until we change to a different animation, whereas other animations should play only once and stop. In addition, some animations should specify another animation to switch to as soon as they have completed.

Animation and Sprite Classes

However, our game logic should not have to deal with all of the intricacies of animation programming. We need to encapsulate the animation logic inside an Animation class.

To begin, we need a way to specify the animation sequences that an Animation object will represent. We could hardcode all of the animation logic, but that would be a lot of work with all of the animals and such, and we would have to throw it away the next time we wrote a game. We could also create a proprietary file format, but then we’d need to write a tool that could take in all of the animation sequences and write them to the file, which is beyond the scope of this book.

Instead, we will use something familiar to Mac developers: the .plist file. You already used a .plist file to set up certain parameters for your Xcode project. Here we will use a .plist file to contain all of our animation sequence data, and we will write an initialization function in our Animation class that will read in that data.

Property list

We can use Xcode to create a .plist file for animation purposes. We will use the emuchick.png file as our texture. Our goal is to describe the “walking left,” “walking up,” “walking down,” and “walking right” animations:

  1. From within Xcode, select File→New File to open the New File dialog.

  2. From the left column select Other and in the right window select Property List.

  3. Type Animations.plist for the name and click the Finish button.

  4. Select the Animations.plist file within Xcode. The .plist editor view will appear.

All items in a property list are added to the root dictionary item, in key-value pairs. You can structure your data by nesting dictionaries or arrays. We are going to structure our animation data by using a dictionary for each sprite texture.

We will begin by adding a dictionary named emuchick.png:

  1. Right-click the Root row and select Add Row.

  2. Replace the “New Item” text with “emuchick.png” in the Key column.

  3. Change the Row type to Dictionary by clicking the Type column (it is set to String by default) and selecting Dictionary.

Now we can start adding rows to the emuchick.png dictionary to describe the animations found in this sprite texture:

  1. Add a row to the emuchick.png dictionary by right-clicking and selecting Add Row. Make sure you added the row to our dictionary, not the root: the new row should be indented farther to the right than the emuchick.png row is in the Key column.

  2. Name the new row “frameCount” and set its type to Number.

  3. Set the value for frameCount to 15, since there are 15 frames in the emuchick.png texture.

Next, we want to store information about all of the different animations that can be found in this sprite. For each separate animation, we will create a new dictionary below the emuchick.png dictionary:

  1. Add another row to the emuchick.png dictionary by right-clicking and selecting Add Row again.

  2. Name this one “walkLeft” and set its type to Dictionary. It will represent an animation of the emu chick walking to the left.

Each animation will need to specify a sequence of frames and the animation period for each of those frames. For “walkLeft”, we want to use a frame sequence of 0,1,2,3,4 with 100 ms for each frame:

  1. Add two rows to the walkLeft dictionary and name them “anim” and “time”. Leave their types as String.

  2. In “anim”, set the value to “0,1,2,3,4”, with no spaces.

  3. In “time”, set the value to “100,100,100,100,100”, also with no spaces.

Great; now we have the first sequence done. For the next two, we can actually copy and paste the walkLeft dictionary and rename it to save us some time:

  1. Right-click on “walkLeft” and select Copy.

  2. Left-click on “emuchick.png”, then right-click and select Paste (it is important to have it highlighted by left-clicking before pasting, or your new copy will end up in the wrong place). Double-check that the new copy is a child of emuchick.png; it should be at the same indentation as the original walkLeft dictionary.

  3. Rename the first new copy to “walkup” and change the “anim” value to “5,6,7,8,9”.

  4. Rename the second copy to “walkdown” and change its “anim” value to “10,11,12,13,14”.

Now we need a walking right animation. But the sprite has no frames of the emu chick walking to the right. Instead, following our plan when we asked the artist to draw the images, we’ll flip the left-facing frames for use in the walking right animation:

  1. Copy and paste the walkLeft dictionary onto emuchick.png once more.

  2. This time, add a row to walkRight, named “flipHorizontal”.

  3. Set its type to Boolean and toggle the checkbox that shows up in the Value column. It should be marked with a check, which means the value is set to true.

The last piece of data we need to store about the emuchick.png animations is an offset value for all of the frames. When we want to draw the emu chick, we don’t want to have to figure out how big the frame is and draw from the upper-left corner; instead, we set that value here so that we can automatically offset the graphics. In this case, we want the origin to be at 16,6 within the frame:

  1. Add one more row to the emuchick.png dictionary.

  2. Name this one “anchor” and keep its type as String.

  3. Set its value to “16,6”.

Now we have a .plist file that contains all of the data to represent a series of animations used by an entity. Next, we will create an Animation class that will allow us to use this data in our game.

Consider, however, that we do not want to allocate a new Animation object for each of our entities. For instance, let’s say we have three siren salamanders walking around in our level. Just as they are all using the same GLTexture for their image data, they should also use the same Animation object for their animation data. You should consider an Animation object as a resource such as a texture.

However, certain aspects of the animation should belong to each Salamander entity: specifically, the current animation sequence being rendered, and the time since the start of the animation.

To facilitate this, we will also create a Sprite class. This class will keep track of the animation data, as well as the exact frame and start time of the animation being rendered.

Animation class

Although the Animation class will represent the animation used by an object in our game, it’s possible that a single object could have multiple behaviors. In the preceding example, the emu chick had only a walking animation, but it could also have had an idle animation or a sleeping animation.

We call each of these behaviors an animation sequence, and our Animation class will keep track of multiple AnimationSequence objects.

In the preceding example, the Animation object used by the emu chick would have only one AnimationSequence.

We will start by defining the AnimationSequence class, followed by the Animation class:

//Animation.h
@interface AnimationSequence : NSObject
{
    @public
    int frameCount;
    float* timeout;
    CGRect* frames;
    bool flipped;
    NSString* next;
}

- (AnimationSequence*) initWithFrames:(NSDictionary*) animData
                       width:(float) width
                       height:(float) height;

@end

@interface Animation : NSObject {
    NSString* image;
    NSMutableDictionary* sequences;
    CGPoint anchor;
}

- (Animation*) initWithAnim:(NSString*) img;
- (void) drawAtPoint:(CGPoint) point
         withSequence:(NSString*) sequence
         withFrame:(int) frame;

-(int) getFrameCount:(NSString*) sequence;
-(NSString*) firstSequence;

-(AnimationSequence*) get:(NSString*) sequence;

@end

The AnimationSequence class keeps track of the number of frames, the time that each frame should be displayed, and (similar to the Tile class) a CGRect to represent the subsection of the GLTexture that represents each frame. It also keeps a Boolean to determine whether the frame should be drawn flipped on the horizontal axis.

The Animation class keeps a string that represents the name of the GLTexture for use with our ResourceManager’s getTexture: function, as well as an NSMutableDictionary to keep track of our AnimationSequences. To make things easy, we will use a string as the key value. When we want to access the walking animation sequence, all we have to do is call [sequences valueForKey:"walking"]. Finally, we keep an anchor point that allows us to draw the animation at an offset.

The AnimationSequence class we defined had only one function, an initialization method, which will parse the animation sequence data from a .plist file:

//Animation.mm
@implementation AnimationSequence

- (AnimationSequence*) initWithFrames:(NSDictionary*) animData
                       width:(float) width
                       height:(float) height
{
    [super init];
    NSArray* framesData = [[animData valueForKey:@"anim"]
                            componentsSeparatedByString:@","];
    NSArray* timeoutData = [[animData valueForKey:@"time"]
                             componentsSeparatedByString:@","];
                            //will be nil if "time" is not present.
    bool flip = [[animData valueForKey:@"flipHorizontal"] boolValue];
    self->next = [[animData valueForKey:@"next"] retain];
    frameCount = [framesData count];
    frames = new CGRect[frameCount];
    flipped = flip;
    for(int i=0;i<frameCount;i++){
        int frame = [[framesData objectAtIndex:i] intValue];
        int x = (frame * (int)width) % 1024;
        int row = (( frame * width ) - x) / 1024;
        int y = row * height;
        frames[i] = CGRectMake(x, y, width, height);
    }
    timeout = NULL;
    if(timeoutData){
        timeout = new float[frameCount];
        for(int i=0;i<frameCount;i++){
            timeout[i] = [[timeoutData objectAtIndex:i] floatValue] / 1000.0f;
            if(i > 0) timeout[i] += timeout[i-1];
        }
    }
    return self;
}

- (void) dealloc {
    delete frames;
    if(timeout) delete timeout;
    [self->next release];
    [super dealloc];
}

@end

We begin by grabbing the array of animation frames labeled “anim” from inside the animData dictionary. We do this by passing @"anim" as the key into the valueForKey: function of NSDictionary, which returns a string value. In the same line, we split that string into substrings by separating each section marked by a comma using the componentsSeparatedByString: method of NSString. We do the same thing on the next line, only we grab the “time” entry instead.

We also grab the “flipHorizontal” entry and convert it to a bool value from which we directly initialize our flipped member variable. After that, we get the “next” entry to determine what the next animation sequence should be: if the value is nil, we will simply stop animating; otherwise, we will continue into the next animation sequence automatically.

Next, we want to create a CGRect that represents the section of the GLTexture each frame represents. We know the width and height of the CGRect because they were passed into our function and each frame is the same size. All we need to do is calculate the x and y offsets based on the frame number of each animation.

We index the framesData array using the objectAtIndex: function and convert each element to an integer to get the frame index. Next, we calculate the x and y offsets based on the frame height and width. Remember, we need to wrap the frames if the width or height is larger than 1,024.

After we are done creating the frame rectangles, we initialize our timeout array. After indexing the timeoutData array and converting each element into an int (the same as we did earlier), we divide the result by 1,000. We mentioned earlier that the iPhone counts time by seconds, although it allows fractional values. Because the time periods in our .plist are in milliseconds, dividing by 1000.0f converts from integer milliseconds to floating-point seconds, which is what we want.

Now we can implement the Animation class. Recall that the Animation class is a resource that multiple Sprite classes will use; it should not store any data used to render a particular instance of an animation, only the list of AnimationSequences. The drawing function should have parameters that specify exactly which frame of which AnimationSequence is currently being requested so that multiple Sprites can render the same animation at different locations of the screen and of the animation.

Let’s start by looking at the initialization function:

//Animation.mm
- (Animation*) initWithAnim:(NSString*) img {
    NSData* pData;
    pData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle]
                pathForResource:@"Animations" ofType:@"plist"]];
    NSString *error;
    NSDictionary* animData;
    NSPropertyListFormat format;
    animData = [NSPropertyListSerialization propertyListFromData:pData
                mutabilityOption:NSPropertyListImmutable
                format:&format
                errorDescription:&error];

    animData = [animData objectForKey:img];

    GLTexture *tex = [g_ResManager getTexture:img];
    image = img;

    float frameWidth, frameHeight;

    if([animData objectForKey:@"frameCount"]){
        int frameCount = [[animData objectForKey:@"frameCount"] intValue];
        frameWidth = [tex width] / (float)frameCount;
        frameHeight = [tex height];
    }

    if([animData objectForKey:@"frameSize"]){
        NSArray* wh = [[animData objectForKey:@"frameSize"]
                        componentsSeparatedByString:@"x"];
        frameWidth = [[wh objectAtIndex:0] intValue];
        frameHeight = [[wh objectAtIndex:1] intValue];
    }

    //anchor is the position in the image that is considered the center. In
    //pixels. Relative to the bottom left corner. Will typically be positive.
    //all frames in all sequences share the same anchor.
    NSString* anchorData = [animData valueForKey:@"anchor"];
    if(anchorData){
        NSArray* tmp = [anchorData componentsSeparatedByString:@","];
        anchor.x = [[tmp objectAtIndex:0] floatValue];
        anchor.y = [[tmp objectAtIndex:1] floatValue];
    }

    NSEnumerator *enumerator = [animData keyEnumerator];
    NSString* key;
    sequences = [NSMutableDictionary dictionaryWithCapacity:1];

    while ((key = [enumerator nextObject])) {

        NSDictionary* sequencedata = [animData objectForKey:key];
        if (![sequencedata isKindOfClass:[NSDictionary class]]) continue;

        AnimationSequence* tmp = [[AnimationSequence alloc]
                              initWithFrames:sequencedata
                              width:frameWidth
                              height:frameHeight];

        [sequences setValue:tmp forKey:key];
        [tmp release];
    }

    [sequences retain];

    return self;
}

The initWithAnim: function accepts the name of an image as its only parameter. This will be used to grab the associated animation from within the Animations.plist file, as well as the GLTexture by the same name.

We start by using an NSData pointer to grab the contents of the Animations.plist file. Next, we convert it into an NSDictionary for easy access of the data inside using the propertyListFromData: method of the NSPropertyListSerialization utility class.

However, the NSDictionary we just got contains all of the animations in the .plist file, and we want only the animation that corresponds to the image name passed into our initialization function. We grab the subsection with [animData objectForKey:img], reassigning it to our NSDictionary pointer. We don’t need to worry about a memory leak while doing this because the original NSDictionary returned by propertyListFromData: is put into an auto-release pool before it’s handed to us.

Now that we have only the data that pertains to this animation, we will extract the Animation and AnimationSequence information from it. For the Animation class, we need the data that applies to all of the AnimationSequences, including the frame height and width and the anchor offset.

The rest of the entries into the dictionary comprise AnimationSequence data. However, we don’t know what their names are or how many there will be, so we need to use an NSEnumerator to iterate across all of the entries. We do this by calling keyEnumerator: on the animData dictionary.

We also need a place to store the AnimationSequences we create, so we initialize our sequences variable as an empty NSMutableDictionary. Since we are calling alloc on sequences here, we need to remember to release it in the dealloc function, as well as all of the entries inside it.

Next, we start the while loop to iterate through all of the keys in our animData dictionary. The entries are not in a particular order, so we need to be sure to skip any entries that we have already read (and therefore are not AnimationSequence dictionaries). We know that any entry that is an NSDictionary will represent an AnimationSequence, so we check the type of entry using isKindOfClass:. If the entry is not an NSDictionary, we skip it.

Finally, we know we have an AnimationSequence dictionary. We grab it from animData using objectForKey: and then send it into a newly allocated AnimationSequence object. Once the AnimationSequence is initialized, we can store it into our sequences dictionary. Since we called alloc on the AnimationSequence when we were creating it, we need to call release. Rest assured, however, that it was retained by NSDictionary when we inserted it into sequences.

And now the rendering function:

//Animation.mm
- (void) drawAtPoint:(CGPoint) point
         withSequence:(NSString*) sequence
         withFrame:(int) frame
{
    AnimationSequence* seq = [sequences valueForKey:sequence];
    CGRect currframe = seq->frames[frame];
    [[g_ResManager getTexture:image]
        drawInRect:CGRectMake(
                point.x+(seq->flipped?currframe.size.width:0)-anchor.x,
                point.y-anchor.y,
                seq->flipped?-currframe.size.width:currframe.size.width,
                currframe.size.height)
         withClip:currframe
        withRotation:0];
}

Compared to the initialization function, rendering is fairly simple. We first grab the AnimationSequence as detailed by the sequence parameter. We then extract the CGRect that represents the portion of a GLTexture that contains the frame we want from the AnimationSequence and store it as currFrame.

Next, we grab the GLTexture from the ResourceManager and call drawInRect:, creating the destination rectangle using the utility function CGRectMake and using currFrame as our source rectangle. Creating the destination rectangle is a little tricky because we have to consider whether the animation is flipped and whether there is an anchor offset. Fortunately, the math is simple, as you can see.

Sprite class

Now that we have an Animation, we can create a Sprite class that will allow us to use it. The Sprite class needs to keep track of what animation it is associated with, which AnimationSequence is currently being drawn, when that sequence started, and what the current frame is.

Because the Sprite class will be used directly in our game code, we are going to make an autorelease constructor instead of the typical init function. We will also need a rendering function that accepts a position to draw the sprite, an update function that will calculate when the current frame needs to change based on the timeout values from the current AnimationSequence, and finally, a way to set which sequence we want to display:

//Sprite.h
@interface Sprite : NSObject {
    Animation* anim;
    NSString* sequence;
    float sequence_time;
    int currentFrame;
}

@property (nonatomic, retain) Animation* anim;
@property (nonatomic, retain) NSString* sequence;

+ (Sprite*) spriteWithAnimation:(Animation*) anim;
- (void) drawAtPoint:(CGPoint) point;
- (void) update:(float) time;

@end

Notice the @property tag we are using for anim and sequence variables. It will cause a special setter function to be called whenever we assign anything to anim or to sequence. Since retain is one of the keywords, it will automatically be retained when we set these.

Although the default setter is acceptable for anim, we want to specify our own setter function for sequence because whenever the sequence changes we need to reset the sequence_time and currentFrame back to zero. We do this by overloading the setter function named setSequence in the following code.

The implementation of Sprite follows:

//Sprite.m
+ (Sprite*) spriteWithAnimation:(Animation*) anim {
    Sprite* retval = [[Sprite alloc] init];

    retval.anim = anim;
    retval.sequence = [anim firstSequence];

    [retval autorelease];
    return retval;
}

- (void) drawAtPoint:(CGPoint) point {
    [anim drawAtPoint:point withSequence:sequence withFrame:currentFrame];
}

- (void) update:(float) time{
    AnimationSequence* seq = [anim get:sequence];

    if(seq->timeout == NULL){
        currentFrame++;
        if(currentFrame >= [anim getFrameCount:sequence]) currentFrame = 0;
    } else {
        sequence_time += time;
        if(sequence_time > seq->timeout[seq->frameCount-1]){
            sequence_time -= seq->timeout[seq->frameCount-1];
        }
        for(int i=0;i<seq->frameCount;i++){
            if(sequence_time < seq->timeout[i]) {
                currentFrame = i;
                break;
            }
        }
    }
}

- (void) setSequence:(NSString*) seq {
    [seq retain];
    [self->sequence release];
    self->sequence = seq;
    currentFrame = 0;
    sequence_time = 0;
}

The constructor function needs to be statically accessible (you call it before an object exists, not on an object), so we use + instead of in the definition. It starts by allocating and initializing a Sprite object.

We then set the animation to the anim variable sent in the parameter. Remember that we declared this as a retain parameter, so it will automatically be retained when we set this, meaning we need to release it in our dealloc function.

We start the first AnimationSequence by using a utility function to ask the Animation which we should use first and then setting it to sequence. This is also a property, but it will be calling our custom setter, the setSequence: function.

Finally, we add the new Sprite to an autorelease pool and return it.

The rendering function is very simple: just pass the point to draw at, the current sequence, and the current frame into the Animation drawAtPoint: function we discussed earlier.

The update function needs to start by grabbing a pointer to the AnimationSequence we are currently running. Next, we need to check that it has a list of frame times; if it does not, we simply increment the frame every update loop. If we reach the end of the AnimationSequence, we start over from the first Animation.

If there is frame time information, we need to know whether the current frame should be incremented. The time parameter of the update function represents the amount of time that has elapsed since the last call to update, so we can simply add this to the sequence_time variable and check whether it is larger than the timeout value of the current frame. We calculate the current frame based on the total sequence_time elapsed.

The setSequence: function is our overloaded setter function, as we already mentioned. A normal setter function has two jobs: retain the new object being assigned to, and release the old one. Our custom setter will also initialize the currentFrame and sequence_time to zero so that the new sequence starts at the beginning.

Physics

In our game, physics will be a simple matter of entity-to-world and entity-to-entity collision in two dimensions. In Level 4, we have water tiles, but that will be a special case of entity-to-world.

Entities

What are “entities”? As we mentioned in Physics Engine, the game world consists of the “level,” the “player,” and the “animals.” To simplify things, we will say the game world consists of the “level” and “entities.” An entity is anything that exists in the game world within a particular level.

The player is an entity because she is in the world but can move around independently. The animals are also entities that can move around on their own. There can also be stationary entities, such as a button. Because a button has an animation and logic behind it (pressed and unpressed), it cannot be fully represented as merely a part of the tile-engine portion of the level; it must be an entity.

All entities need to have a position, a way to render themselves, and an update function that gets called every frame to allow their internal logic to progress.

We will begin by creating a class named Entity (which you already saw referenced in TileWorld):

//Entity.h
@interface Entity : NSObject {
    CGPoint worldPos;
    CGPoint velocity;
    Sprite* sprite;
    TileWorld* world;

}

@property (nonatomic, retain) Sprite* sprite;
@property (nonatomic) CGPoint position;

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)sprite;
- (void) drawAtPoint:(CGPoint) offset;
- (void) update:(CGFloat) time;
- (void) setWorld:(TileWorld*) newWorld;

@end

Notice the pointer to TileWorld. This allows the entity to check for entity-to-world collisions on its own.

The implementation of the Entity class is very simple, with the exception of the render function, which adds its position to a camera offset before calling drawAtPoint: on its Sprite:

//Entity.m
- (void) drawAtPoint:(CGPoint) offset {
    offset.x += worldPos.x;
    offset.y += worldPos.y;
     [sprite drawAtPoint:offset];
}

The rest of the functions are simple or empty because Entity is a base class. We will create other classes that inherit from Entity and implement more interesting functionality.

Entity-to-World

Entities that can move but are limited by the physical properties of the level must detect entity-to-world collisions. But first we must have a way to know what physical property each level tile has.

We do this in a way similar to the unique tile indexes of the tile graphics. Just as we had a file with texture indexes for each tile, we can have a file with physics flags for each tile. Our game design incorporates the following physics properties for tiles: normal, impassable, and water. Normal is applied to tiles that can be walked upon, impassable is used for walls and tiles that are otherwise inaccessible to entities, and water is for special tiles that represent water.

Giving values (0 = normal, 1 = impassable, 2 = water), we could represent the first level with the following data:

1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1

Not very exciting, but you get the point. Entities cannot enter any of the tiles with a “1,” but they can walk freely in any tile with a “0.”

Now that we know what each tile is supposed to represent physically, we can perform entity-to-world collision detection (remember the separation between collision “detection” and collision “resolution”).

The most naive approach is to do a rectangle-to-rectangle collision between the entity and each level tile. However, this quickly becomes inefficient as you have to perform levelWidth*levelHeight checks for all entities at every frame. Fortunately, we can make a number of optimizations.

Since this check will be performed every update loop, we can assume that our entity is in a stable state at the start of our check (because any collisions would have been resolved during the last frame). Therefore, if the entity is not moving (its velocity is zero), we can skip the process altogether (for the current loop).

Meanwhile, if the entity is moving, we only need to check for collisions along its intended path. Even better, if we know that our entities aren’t moving faster than one tile length per frame, we only need to check the end of the path on each frame (because it will give the same result as the middle of the path if we’re not passing through more than one tile).

Unfortunately for us, the iPhone SDK does not come with a helpful 2D vector class. Therefore, we have to provide our own functions for vector math located in PointMath.h:

add

Performs vector addition on two CGPoint vectors

sub

Performs vector subtraction on two CGPoint vectors

scale

Performs scalar multiplication between a CGPoint and a scalar float

distsquared

Returns the squared magnitude of a CGPoint vector

unit

Returns a unit vector based on a CGPoint vector

towards

A utility function that gets the vector between two CGPoints and derives its unit vector

The following is an example of how we will check for collisions in our entity code using our functions for vector math (remember that the time parameter of our update function represents time passed since the last update):

CGPoint distThisFrame = scale(velocity, time);
CGPoint projection = add( worldPos, distThisFrame );
Tile* newTile = [world tileAt:projection];
if( overtile == nil || (overtile->flags & UNWALKABLE) != 0 ) {
    //tile is out of bounds or impassable
    projection = worldPos; //don't move
}
worldPos = projection;

First we calculate the amount of space that will be moved during this frame. Note that we scale our velocity by the time passed during the last frame, instead of merely using the entire velocity. This is because our speed is in units per second, but our update function is not called once per second (hopefully it’s being called 33 times per second). If we used the full velocity, the entities would be moving far too quickly, and more importantly, they would speed up or slow down if our game loop was faster or slower.

Next, we use vector math to add the distThisFrame to our current worldPos to get the projected position at the end of the current loop. Now we need to check to see whether that projected point is going to be in a safe tile or if it is impassable.

We use the tileAt: function of TileWorld to grab the tile that is underneath that point. If the point is out of the bounds of the level, it will return nil.

We check to see whether the result is nil or whether it has an UNWALKABLE physics flag. If either of these cases is true, we should not let the entity walk onto the tile, so we set the projection to our current position.

Finally, we accept the projection vector as our new current position.

Special Tiles

Although impassable tile collisions can simply be resolved as soon as they are detected, other special types of tiles need extra game logic. To handle these situations, we can send the entity a message when it collides with a certain type of tile. This allows us to write code to allow entities to handle special tiles in their own way.

If a hippopotamus is walking on land and collides with a water tile, it can move to a swimming state and keep going. In contrast, a cougar might decide it doesn’t want to get wet and the AI can use the message to turn the cougar around and find another path.

Entity-to-Entity

Entity-to-world collision is fairly straightforward. Your only goal is to keep entities from entering impassable tiles, and notifying them when they’re on special tiles such as water. Entity-to-entity collision, however, is more complex.

With entity-to-entity collision, there are many results from different types of collisions. If an animal touches another animal, we could simply choose not to let the animals pass through each other, or we could ignore the situation if we want to allow animals to walk over each other. Or if they are two different types of animals, they might attack each other.

When collision resolution takes many different forms due to game design, the line between physics code and game logic code gets blurred. Similar to special tiles, we can handle this through special messages alerting entities of events so that the entities themselves can handle the resolution logic.

However, we might also want to resolve these collisions strictly at the GameState level. A single entity knows only as much about the world as we give it access to. However, the GameState envelops the entire world, including all of the entities, the level, and all of the game logic code.

To clarify, consider the following example. For the first level, we want to determine when one of the emu chicks has walked onto the nest at the top of the level. To represent the nest, we will have a special kind of entity called a trigger area. A trigger area has a bounding box for determining physical collisions just like a regular entity, but it doesn’t necessarily have a graphical representation. Because the nest is already represented in the tiles of the level background, we don’t need to draw anything for this trigger area. Instead, we merely perform entity-to-entity collision between emu chicks and the nest trigger area.

However, when we detect this collision, we want to remove the emu entity from the level. Simply sending a message to the entity won’t allow it to remove itself. We need the resolution to happen at the GameState level.

In addition, not all entities require collision detection against each other. Using the previous example, although an emu chick should be tested for collision against the nest trigger area, the main character should not be. Because of the wide variety of expected results from entity-to-entity collision, it should be handled on a case-by-case basis.

In the next section, we will examine the implementation of these cases.

Level 1 Implementation

As we stated before, the goal for Level 1 is for the player to control the main character in a way that will herd a flock of emu chicks into their mother’s nest.

This will require the following resources:

  • A tile level with a clear depiction of a nest and a wide space for the chicks to run around in

  • A main character sprite with idle and walking animations

  • An emu chick sprite with idle and running animations

  • A mother emu sprite that will pace back and forth on the nest

The game logic will include detecting when an emu chick has walked onto the nest and removing it from the world when it does, a nest trigger area for determining when emu chicks have reached the nest, and a level complete state transition when all emu chicks have reached the goal.

AI logic will consist of:

  • A very basic emu mother entity that walks back and forth on the nest

  • An emu chick entity that will flock together with other emu chick entities and try to run away from the main character

User input will include detecting a single touch on the screen from the player, translating that into world coordinates to detect the target square, and setting the main character’s velocity to move toward that square.

gsEmuLevel

To hold our tile engine, entities, and game logic for the first level, let’s create a class named gsEmuLevel that inherits from the GLESGameState class.

gsEmuLevel will need to keep track of the TileWorld, the main character entity, the emu flock entities, and the emu mother entity, as well as the level’s complete state:

@interface gsEmuLevel : GLESGameState {
    TileWorld* tileWorld;
    Tom* m_tom;
    EmuMother* mama;
    Emu** flock;
    int flock_len;
    BOOL win;
}
@end

TileWorld

In the constructor, we initialize the TileWorld object with the level data:

- (id) initWithFrame:(CGRect)frame andManager:(GameStateManager*)pManager;
{
    if (self = [super initWithFrame:frame andManager:pManager]) {
        [self setupWorld];
    }
    return self;
}

- (void) setupWorld {
    tileWorld = [[TileWorld alloc] initWithFrame:self.frame];
    [tileWorld loadLevel:@"lvl1_idx.txt" withTiles:@"lvl1_tiles.png"];

}

In the render function of our game state, we tell the TileWorld to draw itself:

- (void) Render {

    //clear anything left over from the last frame, and set background color.
    glClearColor(0xff/256.0f, 0x66/256.0f, 0x00/256.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    [tileWorld draw];

    [self swapBuffers];
}

Now that we have a level to walk around in, we need something to walk around in it.

Main Character

To start, we need the main character sprite. We want the main character to walk around or stand still, so we need walking and idle frames for all facings. Again we will use the draw flipped technique for the right-facing frames so that we need only up, down, and left frames on the texture. Figure 4-4 shows the sprite representing Tom O’Reilly.

Tom O’Reilly sprite
Figure 4-4. Tom O’Reilly sprite

Once we have the sprite, we can create an Entity class to wrap around it. Then we can put this Entity object into gsEmuLevel where we will initialize it and hook it into the render and update loops. And since Tom is going to be controlled by the player directly, we can hook it up to the user input code as well.

Entity

The Entity class that represents Tom will be named, well, Tom. It is actually quite a simple class, as most of the rendering code will already have been handled by the Entity class it inherits from, and the entity logic will simply be to move toward a square designated by the player:

@interface Tom : Entity {
    CGPoint destPos;
}

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr ;
- (void) moveToPosition:(CGPoint) point;

@end

The initWithPos function simply calls the Entity initWithPos function and initializes the destPos to be equal to the current worldPos (otherwise, it would be moving when the game loop starts).

The moveToPosition function will be called by the user input code of gsEmuLevel and will simply set the destination position to the given point in world coordinates.

Tom also overrides the update: function inherited from Entity, like this:

//Tom.m
- (void) update:(CGFloat) time {
    float xspeed = 200*time, yspeed=200*time; //pixels per second.

What is that 200 value? Uh-oh, that’s a magic number! We should replace that with a define such as PLAYER_SPEED. Notice how we multiply it by time (like the distThisFrame example earlier) to get the scaled speed per frame. Next, we save the current worldPos in case our attempt at moving fails (because of a wall or the edge of the level):

CGPoint revertPos = worldPos;

And now we project the entity toward the destination position, first on the x-axis followed by the y-axis. If our speed causes us to go past the square (fabs(dx) < xspeed), we snap the distance traveled to end exactly at the destination position. Otherwise, we multiply the speed by positive or negative 1 depending on whether we’re moving “right” or “left.”

    float dx = worldPos.x - destPos.x;
    if(dx != 0){
        if(fabs(dx) < xspeed){
            worldPos.x = destPos.x;
        } else {
            worldPos.x += -sign(dx)*xspeed;
        }
    }

    float dy = worldPos.y - destPos.y;
    if(dy != 0){
        if(fabs(dy) < yspeed){
            worldPos.y = destPos.y;
        } else {
        worldPos.y += -sign(dy)*yspeed;
        }
    }

Now we check to see whether we have moved to an acceptable tile. If not, we don’t want to simply stand still; it would be better to try to slide along the edge of whatever obstacle we have reached. First, we project ourselves using only the new x value, keeping our old y value as though we had no intention of moving in any direction but horizontal; if that projection works, we accept it and move on. If not, we try to project our position only in the y direction; again, if that works, we accept it and move on. Finally, if neither attempt works, we accept our failure and revert to the old position:

    if(![world walkable:worldPos]){
        if([world walkable:CGPointMake(worldPos.x, revertPos.y)]){
            worldPos = CGPointMake(worldPos.x, revertPos.y);
        }
        else if([world walkable:CGPointMake(revertPos.x, worldPos.y)]){
            worldPos = CGPointMake(revertPos.x, worldPos.y);
        }
        else {
            worldPos = revertPos;
        }
    }

Now that we know what the new position will be, we should recalculate the way our sprite is facing and update the animation sequence:

    //calculate current direction
    NSString* facing = nil;
    if(dx != 0 || dy != 0){
        if(fabs(dx) > fabs(dy)){
            if(dx < 0) {
                facing = @"walkright";
            } else {
                facing = @"walkleft";
            }
        } else {
            if(dy < 0){
                facing = @"walkup";
            } else {
                facing = @"walkdown";
            }
        }
    } else {
        facing = @"idle";
    }

    //if the direction has changed, we also change our animation
    if(![sprite.sequence isEqualToString:facing])
    {
        sprite.sequence = facing;
    }

Finally, we tell our sprite to update:

    [sprite update:time];
}

gsEmuLevel

Now that we have filled out our entity, we can plug it into our level. We already have the m_tom variable defined in the header; all we need to do is use it.

First, we initialize it inside the setupWorld: function by loading the animation, initializing an instance of Tom, and assigning it to our m_tom variable. Then we add m_tom to tileWorld’s list of entities to render and we set the camera to point at m_tom’s worldPos:

//gsEmuLevel.m
Animation* tomanim = [[Animation alloc] initWithAnim:@"tom_walk.png"];
m_tom = [[Tom alloc] initWithPos:CGPointMake(100, 100)
                    sprite:[Sprite spriteWithAnimation:tomanim]];
[tileWorld addEntity:m_tom];
[tileWorld setCamera:m_tom->worldPos];

Next, inside Update:, we call m_tom’s update: function.

We don’t need to add m_tom to the Render: function because TileWorld will have already taken care of it because we added him to the list of entities to render.

Finally, we need to be sure to call release on m_tom in the destructor.

User input

The user input code is very simple. We will receive a call to touchesEnded: when the player taps the screen.

We can get the position (in screen coordinates) of the tap by calling [self touchPosition:touch]. We then convert that position from screen coordinates to world coordinates using the worldPosition: helper function we wrote in the TileWorld class.

Finally, we set the new destination (in world coordinates):

[m_tom moveToPosition:[tileWorld worldPosition:[self touchPosition:touch]]];

Emu Chicks

Now we’ll let loose the sprite for emu chicks that we introduced in “Animation.” We attach that to a new class called Emu which will also inherit from the base Entity class. This will allow us to write the AI code that controls the emus inside the Emu entity.

We want our emu chicks to flock together. There are many ways to implement a flocking algorithm, but they should all exhibit some common behavior:

  • Don’t stay too close to neighbors at short range.

  • Calculate a velocity based on the average velocity of neighbors.

From here, based on the type of flocking behavior you wish to exhibit, you can add more rules. For example, if you wish to keep a tight cloud-shaped grouping, you may add:

  • Try to move toward the center of mass of the flock.

In addition to the flocking behavior, we also want to assert two more rules:

  • If the main character is nearby, run away from him.

  • If the emu mother is nearby, run toward her.

When we write this code in the Emu entity and create multiple instances, we should see emergent behavior from the entities in the form of a flock of emus that will run away from the main character, and run toward their mother if they are in close proximity.

These rules may seem to be in conflict with each other, and in some cases they are; but resolving that conflict is actually rather easy. Since each rule dictates a velocity, we can consider them to all be impulses being added to the emu entity’s current velocity. When we calculate all of them and add them together, we get a single velocity that represents the direction in which the emu entity should move.

Entity

The Emu entity class will need a velocity vector to represent its speed and direction. When we calculate our flocking, however, we want to count only the current velocity before applying any new velocities based on the result of the flocking algorithm; therefore, we need to keep a temporary velocity used for flocking calculation, which we name nextVel.

Finally, we also want to add a velocity to individual emus to keep them away from the other emus. That is what collision_tweak is for:

@interface Emu : Entity {
    CGPoint velocity;
    CGPoint nextVel;
    CGPoint collision_tweak;
}

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr;
- (void) flockAgainst:(Emu**) others count:(int)count;
- (void) avoidPlayer:(Entity*) mainChar;
- (void) runToGoal:(Entity*) emuMother;
- (void) update:(CGFloat) time;

@end

Notice that we have three separate AI functions here, in addition to the update function. The first one, flockAgainst, definitely needs to be separate from update: because it needs to be called on each entity separately before the individual entity behavior should take place.

The next two, avoidPlayer: and runToGoal:, could technically be wrapped into update: if we needed them to be. However, they can each act individually, and therefore make a cleaner design as separate functions; if we were to simply omit a call to one of them, you would still see all of the other set of behaviors you expected to see.

The implementation of flockAgainst: is a bit complicated. To start, we will utilize 95% of our current velocity in our next velocity. This represents the fact that things such as changes in flock movement do not happen immediately, but gradually over time. Otherwise, you might see a flock of birds moving together in the sky immediately freeze, then drop to the ground and land:

//Emu.m
- (void) flockAgainst:(Emu**) others count:(int)count {

    nextVel = scale(velocity, 0.95f); //carry inertia

    int neighbor_count=0, vel_count = 0;
    CGPoint avg_pos = CGPointMake(0,0), avg_vel=CGPointMake(0,0);
    collision_tweak = CGPointMake(0,0);

Next, we will perform calculation of short-range repulsion of neighbors. Rather than repulsing against all emus, we find the nearest and move away from only that one.

For each emu in the flock (not counting ourselves), we first grab the distance between the current emu and the one being flocked against. We keep track of which emu in the flock is closest to this emu, and we also count the average velocity of all flock members that are within two tiles of the current emu. In addition, we count the average position of all flock members within four tiles of the current emu.

By considering only average velocity and positions of emus within a certain radius, we can separate our emus into smaller groups that still flock independently of each other. If those two subflocks were to get close to each other, they would merge into one giant flock again:

    float nearest_dist=0.0f;
    Emu* nearest_emu;
    float velCutoff = 2*TILE_SIZE;
    float posCutoff = 4*TILE_SIZE;
    for(int i=0;i<count;i++){
        if(self == others[i]) continue;
        float distSQ = distsquared(worldPos, others[i]->worldPos);
        if(distSQ < nearest_distSQ || nearest_dist == 0)
        {
            nearest_distSQ = distSQ;
            nearest_emu = others[i];
        }
        if(distSQ < velCutoff*velCutoff)
        {
            vel_count++;
            avg_vel = add(avg_vel, others[i]->velocity);
        }
        if(distSQ < posCutoff*posCutoff)
        {
            neighbor_count++;
            avg_pos = add(avg_pos, others[i]->worldPos);
        }
    }

The preceding code updates our knowledge of the flock as seen from the point of view of the current entity:

  • We know which chick is closest, storing it from the others array into next_emu.

  • We know the number of velocities to count from other chicks (storing it in vel_count) and the average velocity of all the nearby chicks (in avg_vel).

  • We know the number of chicks in the range we need to consider (storing it in neighbor_count for positions and vel_count for velocities) and the average of all their positions and velocities (in avg_pos and avg_vel).

Next, we calculate a velocity that will push us away from the nearest emu so that we maintain a comfortable distance:

    float emuradius = TILE_SIZE/2;
    if(nearest_dist < emuradius*emuradius) {
        CGPoint away = toward(nearest_emu->worldPos, worldPos);
        float overlap = emuradius - sqrt(nearest_dist);
        collision_tweak = scale(away, overlap*0.25f);
    }

Then we try to move toward the average flock position. Technically, we were calculating only the total position and velocity earlier. We still need to divide it by the number of neighbors to get the average. Once we know where the average position is, we look at our distance from that point and ignore it if we’re too close or too far from the flock center. Otherwise, we add a velocity toward the flock center, but we scale it based on how far away it is so that if the emu is close it only waddles, but if the emu is far away it hurries to catch up:

    //make the sum into an actual average.
    avg_pos = scale(avg_pos, 1.0f/neighbor_count);
    CGPoint to = toward(worldPos, avg_pos);
    float distToFlock = sqrt(distsquared(worldPos, avg_pos));
    if(distToFlock > 34 && distToFlock < 128){
        CGPoint accel = scale(to, sqrt(distToFlock −32));
            //using a sqrt(distance) relationship here...
           // (distance) was a bit too exaggerated.
        nextVel = add(nextVel, accel);
    }

Finally, we want our velocity to emulate the overall flock velocity. However, we want it to represent only a portion of our calculated velocity, so we apply it as a ratio with one part average velocity and 10 parts currently calculated velocity (the repulsion and attraction values we put into nextVel, respectively):

    //attract to neighbors' velocity
    if(vel_count > 0) {
        avg_vel = scale(avg_vel, 1.0f/vel_count); //average velocity.
        //take weighted average between avg_vel and nextVel.
        CGPoint sum = add(scale(avg_vel, 1.0f), scale(nextVel, 10.0f));
        nextVel = scale( sum, 1.0f/11.0f );
       }
    }

Note that this is all stored in nextVel—we don’t actually apply it to our real velocity yet because this function is about to be called on the rest of the emus in the level and we want them to perform their calculations based on our current velocity, not the new one we just calculated for ourselves.

Next, we look at the logic used to run away from the main character. After the previous function, this should seem familiar:

- (void) avoidPlayer:(Entity*) mainChar {
    CGPoint away = toward(mainChar->worldPos, worldPos);
    float dist = sqrt(distsquared(mainChar->worldPos, worldPos));
    if(dist < 64){ //vision radius?
        CGPoint accel = scale(away, 300.0f/dist);
        nextVel = add(nextVel, accel);
    }
}

The function to attract the emu toward the emu mother should also be easy to understand now. There is, however, a slight twist: if we are within 30 units of the goal, we also add a repulsive velocity. This makes the emus stand near the emu mother, but not directly on top of her:

- (void) runToGoal:(Entity*) emuMother {
    CGPoint away = toward(emuMother->worldPos, worldPos);
    float dist = sqrt(distsquared(emuMother->worldPos, worldPos));
    if(dist < 30){ //vision radius
        CGPoint accel = scale(away, 300.0f/dist);
        nextVel = add(nextVel, accel);
    }
    if(dist > 34 && dist < 128){
        CGPoint accel = scale(away, −0.5f*(dist-32));
        nextVel = add(nextVel, accel);
    }
}

Finally, we get to the update: function. This generally looks a lot like Tom’s update: function, except that when the emu encounters an UNWALKABLE tile, it chooses a random direction in which to wander off. We also speed up or slow down the walk animation based on the magnitude of the velocity:

- (void) update:(CGFloat) time{
    CGPoint revertPos = worldPos;
    velocity = nextVel;
    worldPos = add(worldPos, scale(velocity, time));
    worldPos = add(worldPos, collision_tweak);
    float dx = -velocity.x, dy = -velocity.y;

    Tile* overtile = [world tileAt:worldPos];
    if(overtile == nil || (overtile->flags & UNWALKABLE) != 0){
        //can't move here.
        worldPos = revertPos;
        float dir = (random() % 360) / 180.0f * PI;
        float mag = TILE_SIZE*2;
        velocity = PointFromPolarCoord(PolarCoordMake(dir, mag));
    }

    if(dx != 0 || dy != 0){
        NSString* facing = nil;
        if(fabs(dx) > fabs(dy)){
            if(dx < 0) {
                facing = @"walkright";
            } else {
                facing = @"walkleft";
            }
        } else {
            if(dy < 0){
                facing = @"walkup";
            } else {
                facing = @"walkdown";
            }
        }
        if(![sprite.sequence isEqualToString:facing])
        {
            //NSLog(@"facing %@", facing);
            sprite.sequence = facing;
        }
    }
    float pixelsmoved = sqrt(dx*dx+dy*dy);
    //using distance-based animation for emus instead of time-based
    [sprite update:pixelsmoved/1000.0f];
}

gsEmuLevel

We already defined a list of Emu entities named flock. We can now insert the emus into the level. We begin implementing the flock by initializing this array in setupWorld::

//gsEmuLevel.m
flock_len = 10;
Emu** emus;
flock = malloc(flock_len *sizeof(Emu*));
for(int i=0;i< flock_len;i++) {
    emus[i] = [[Emu alloc]
        initWithPos:CGPointMake(200, 15*TILE_SIZE)
        sprite:[Sprite spriteWithAnimation:emuanim]];
    [tileWorld addEntity: emus[i]];
}

In the preceding code, we decided to use 10 emus to represent the flock, so we allocate 10 Emu pointers—but notice that we used malloc(). That is a C function, not an Objective-C function. What gives?

As we mentioned before, you can mix C, C++, and Objective-C if you know what you’re doing. In this case, we will need to remember to use free() to release this array of pointers when gsEmuLevel is deallocated. However, we used alloc for the Emu objects, so first we will iterate through that list and call release on each one.

Next, we iterate through the list and initialize 10 new Emu objects, remembering to add them to TileWorld to be rendered. In Update:, we first call flockAgainst: on each flock member. Once we complete that, we can call avoid:, goal:, and update: to finalize their AI calculations and update their position and sprites:

for(int i=0;i<flock_len;i++){
    [flock[i] flockAgainst:flock count:flock_len];
}
bool winning = true;
for(int i=0;i<flock_len;i++){
    [flock[i] avoid:m_tom];
    [flock[i] goal:mama];
    [flock[i] update:time];
}

Again, we do not need to add anything to Render: because TileWorld will already render all of the emus for us.

Emu Mother

The goal of the level is to lead the emu chicks to the nest where their mother is waiting for them. We will represent the mother as an entity and limit her movements to the area of the nest; therefore, we will not need an additional entity to represent the nest. Simply colliding with the mother will mean the emu chick is over the nest.

All we need spritewise is for the emu mother to walk to the left and right and occasionally perform an idle animation such as looking about (see Figure 4-5).

Emu mother sprite
Figure 4-5. Emu mother sprite

The entity AI will be just as simple as that:

typedef enum EmuMotherState {
    EM_WALKING = 0,
    EM_IDLING,
} EmuMotherState;

//EmuMother is a subclass of Emu, because we want
//  to reuse Emu's update method.
@interface EmuMother : Emu{
    EmuMotherState state;
    float state_timeout;
    CGRect bounds; //where we will wander around.
}

@end

We define an enumeration to represent the two possible states the emu mother can be in: walking and idling. We also inherit from Emu rather than Entity because we want to reuse the Emu’s update function.

We will use the state_timeout variable to determine when to switch between walking and idling.

The bounds CGRect will represent an area the emu mother can walk in (notice that this is being used for AI and not for physics detection).

We initialize the emu mother with a hardcoded bounds area:

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr {
    [super initWithPos:pos sprite:spr];
    bounds = CGRectMake(144,687, 192, 64);
    return self;
}

Next, we need an update: function that can move the emu mother between idle and walking states, and keep her within the bounds rectangle if she is walking.

Because we inherit from Emu we can use all of the same variables for AI, including velocity and nextVel. We begin by setting nextVel to our current velocity and calling the Emu class update: function. This will move our entity forward, stopping if it hits any UNWALKABLE tiles (such as the fences on the left and right sides of the level), and set our walking animation sequence with the appropriate facing. Then we make sure we didn’t walk out of bounds, and snap our position back if we did.

Next, we check to see whether time has expired on our current AI state; if so, we toggle to the next one. If we were previously idling, we choose a random direction and start walking. If we were walking, we start idling:

- (void) update:(CGFloat) time {
    nextVel = velocity;
    CGPoint revertPos = worldPos;
    [super update:time];
    state_timeout -= time;
    if(worldPos.x < bounds.origin.x ||
        worldPos.x > bounds.origin.x+bounds.size.width ||
        worldPos.y < bounds.origin.y ||
        worldPos.y > bounds.origin.y + bounds.size.height
    ){
        //wandered too far.
        worldPos = revertPos;
        state_timeout = 0.0f;
    }
    if(state_timeout <= 0.0f){
        switch (state) {
            case EM_IDLING:
            {
                //pick a random direction for wandering.
                float dir = (random() % 360) / 180.0f * PI;
                float mag = TILE_SIZE*4;
                velocity = PointFromPolarCoord(PolarCoordMake(dir, mag));
                if(fabs(velocity.x) < fabs(velocity.y)){
                    //make primary movement horizontal, because we don't
                    //have up/down walk cycles for the emu mother.
                    float tmp = velocity.x;
                    velocity.x = velocity.y;
                    velocity.y = tmp;
                }
                state = EM_WALKING;
                state_timeout = (random() % 1000) / 1000.0f * 1.5f + 0.5f;
            }
            break;
            case EM_WALKING:
                //idle a while.
                velocity = CGPointMake(0,0);
                state = EM_IDLING;
                state_timeout = (random() % 1000) / 1000.0f * 1.5f + 0.5f;
                break;
        }
    }
}

This behavior looks pretty convincing when we run it.

Game Logic

Now that we have all of the elements necessary, we can write game logic to figure out when the player reaches the win state. In the case of gsEmuLevel, the win state is achieved when all of the Emu entities are near the EmuMother.

We can implement this check in the Update: function of gsEmuLevel by hijacking the Emu update loop and checking the distance from mama for each emu:

bool winning = true;
for(int i=0;i<flock_len;i++){
    [flock[i] update: timeElapsed];
    if( distsquared(mama.position, flock[i].position) >
                    EMU_WIN_DISTANCE*EMU_WIN_DISTANCE)
    {
        winning = false;
    }
}

We begin by assuming the win state is true and proceed by determining whether any of the 10 Emu entities are not close enough to the EmuMother. We do this by checking to see whether the distance between the mama and the current emu is larger than some number EMU_WIN_DISTANCE.

Notice we are doing something strange, though; we’re actually checking whether the distance squared is greater than EMU_WIN_DISTANCE squared. This is a common speed optimization that becomes apparent when you look at the math involved in calculating distance.

The formula for the distance C between points A and B is:

(A.x − B.x)^2 + (A.y − B.y)^2 = C^2

Because we are trying to get C, the code would look like this:

int resultC = sqrt( (A.x - B.x)* (A.x - B.x) + (A.y - B.y)* (A.y - B.y) );

That formula requires four subtractions, two multiplications, one addition, and a call to the sqrt() function. Without getting into CPU architecture, addition and subtraction operations are very fast and a multiplication operation takes only slightly longer, but sqrt() is very slow in comparison.

Fortunately, with a little math magic, we can get rid of the call to sqrt(). The trick is that we don’t care about the actual value of the distance; we care only about whether it is greater than some comparison value. If we simply remove the call to sqrt(), we get C squared, which is fine for our purposes if the value we compare it to is also squared! Therefore, simply comparing distSquared(...) > EMU_WIN_DISTANCE * EMU_WIN_DISTANCE will provide the same results in half the time.

If any of the Emu objects are too far away, our checkWinState will get set to false and when we get out of the loop, we’ll know not to set the win condition to true:

if(winning && ! win) {
    win = true;
    m_tom.celebrating = true;
}

If we have found a winning state and we haven’t already declared ourselves the winner, we set win to true and tell Tom to start celebrating.

Sound

The main sound effect we want for this level is to hear the emu chicks peeping as they search for their mother. Unfortunately, we don’t have an emu farm nearby, so we’re going to have to use a chicken sound instead.

Fortunately, our players aren’t likely to be able to tell the difference. We don’t need an accurate sound; we just need one that makes the player feel like she is chasing a flock of avian babies.

We have added a sound called trimsqueak.mp3 to our project and we will play it looped in the background music for this level. In gsEmuLevel.m, add the following lines of code to the bottom of setupWorld:

    [g_ResManager stopMusic];
    [g_ResManager playMusic:@"trimsqueak.mp3"];

The first line will stop whatever music may have been playing before (from the Main menu), and the second plays the sound we want for this level.

Level 2 Implementation

The goal for Level 2 is for the player to navigate to the back of a cave of sleeping lions, pick up the McGuffin, and return alive.

This will require the following resources:

  • A tile level showing the inside of a cave.

  • The same main character sprite from before, with added death animations.

  • A lion sprite, including sleeping, napping, waking, and attacking animations. We will include both lion and lioness for visual variety, but they will behave the same.

  • A McGuffin object, such as a coin or a star.

The game logic will include determining when the player has reached the McGuffin (“level complete”) or has been eaten by a hungry lion (“level failed”).

AI logic will consist of a lion entity. The lions will be strewn across the cave floor while sleeping, forcing the player to weave between them to get to her goal. Occasionally, they may wake up and look around. If they spot the main character, they will swipe at him; and if he gets hit, the player loses.

User input will be the same as Level 1, with no changes.

gsLionLevel

If our levels were basically the same as far as game logic and user input, with only the tile level and entities changing, we would reuse the same GLESGameState class for each level.

But because each of our levels will exhibit strongly different game logic, and in some cases user input, the best way to represent them is by separating each into its own class.

Therefore, we will create a new class named gsLionLevel that inherits from GLESGameState. We can copy and paste the TileWorld and user input code from gsEmuLevel. When initializing the Tom entity, we can use the same code, just modifying the start position to the bottom of our level:

@interface LionLevel : GLESGameState {
    TileWorld* tileWorld;
    Tom* m_tom;
    Lion** m_lions;
    int lions_length;
    Entity* m_mcGuffin;
}

The Tom class here is exactly the same as the last level. Lion is a new entity class we’ll create to represent the lions in the level. And the rest of this code should be easily recognizable by now, so we’ll move on.

TileWorld

The level we create for our TileWorld class should represent a cave full of lions with a space in the back for the McGuffin to lie, something similar to Figure 4-6.

Lion level
Figure 4-6. Lion level

Again you can see the unique tile strip that will be loaded (see Figure 4-7).

Lion level unique tiles
Figure 4-7. Lion level unique tiles

Here are the contents of the lvl2_idx.txt file which includes the tile indexes and physics flags:

12x20
10,11,9,10,8,9,8,11,10,11,8,10
10,8,9,8,10,11,9,10,8,9,11,8
11,9,11,8,1,1,0,1,10,10,9,8
11,8,8,14,0,0,0,1,15,11,11,10
10,14,1,1,0,1,0,0,1,1,15,9
8,1,1,0,0,0,1,0,0,1,1,8
9,1,1,2,1,2,2,1,0,1,1,11
11,1,1,2,1,1,2,1,0,1,1,8
11,1,1,1,2,1,1,1,0,1,1,10
8,1,2,1,1,2,1,1,2,1,1,8
11,1,2,1,1,1,2,2,1,1,1,11
10,12,2,2,1,1,1,1,1,1,13,10
10,8,12,2,2,1,1,2,1,13,11,9
11,9,8,12,2,2,2,1,13,10,9,11
9,11,10,10,1,2,2,1,9,11,10,8
11,8,7,6,7,6,7,6,7,6,10,8
8,10,4,3,3,4,3,3,3,3,8,11
10,4,5,3,3,3,3,3,3,3,3,8
5,3,3,3,5,3,3,3,3,3,5,3
3,3,4,3,3,3,3,3,5,3,3,3

1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,0,0,0,0,1,1,1,1
1,1,1,1,0,0,0,0,1,1,1,1
1,1,0,0,0,0,0,0,0,0,1,1
1,0,0,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,0,0,1
1,1,0,0,0,0,0,0,0,0,1,1
1,1,1,0,0,0,0,0,0,1,1,1
1,1,1,1,0,0,0,0,1,1,1,1
1,1,1,1,0,0,0,0,1,1,1,1
1,1,0,0,0,0,0,0,0,0,1,1
1,1,0,0,0,0,0,0,0,0,1,1
1,0,0,0,0,0,0,0,0,0,0,1
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0

McGuffin

As we described earlier, the McGuffin is a very simple object. It merely sits in place until the player comes along to pick it up. To that end, we can simply use the Entity class initialized with the McGuffin sprite.

Since there is no AI to deal with, just game logic for detecting player-to-McGuffin collision, we’ll move on.

Main Character

Tom will move and act the same as in the first level, but he’ll need additional code to handle being attacked by lions. We add the ability to play back a dying animation sequence, and some flags to disable moving around while the character is dead.

The dying flag is simply a Boolean, added to Tom.h:

bool dying; //flag for when death animations are playing.
            // set in dieWithAnimation, used in update to modify movement
            // and facing code.  Does not reset.

We then need a method to start the dying process:

//used on player death.
// Caller can pass in special death animations per situation,
// such as drowning or mauling.
- (void) dieWithAnimation:(NSString*) deathanim {
    if(!dying){ //You only die once.
        dying = true;
        sprite.sequence = deathanim;
    }
}

Next, we need to modify Tom’s update method. We need to make sure the character doesn’t move around while dying, and the easiest way to do that is to just reset speed to 0 while in the dying state:

- (void) update:(CGFloat) time {
    float xspeed = 200*time, yspeed=200*time; //pixels per second.

    if(dying){
        //you're not a zombie, hold still.
        xspeed = yspeed = 0;
    }

We also need to make sure the dying animation isn’t clobbered by other character animations. We do this near the end of the update method:

    if(dying){
        //let the dying animation proceed as specified in the animation.plist.
    } else {
        if(![sprite.sequence isEqualToString:facing])
        {
            sprite.sequence = facing;
        }
    }

    [sprite update:time];
}

Lion Entities

The challenge of sneaking around the sleeping lions is the whole point of this level. To make this fun, we need to ensure that the player will have to go near the lions, while also making sure to give the player plenty of visual feedback about the current state of each lion.

Specifically, we will give the lions the following AI states:

Sleeping

While sleeping, the lions will not be awakened no matter how close the main character gets to them. After a random period of time, the lions will move to the Napping state.

Napping

If the player gets close to a napping lion, the lion will move to the Alert state. If the lion is not alerted after a period of time, it will return to the Sleeping state.

Alert

The lion is still lying down, but it has both eyes open and its head up, looking around for the main character. If he is in the lion’s cone of vision, the lion will move to the Attacking state.

Attacking

The lion has spotted the character and is swiping at him. If the lion is near the character, the character will become incapacitated and the player will fail the level.

We will arrange the lions on the level so that there is only a little space between them for the player to creep through. The player will have to wait and watch for the right time to move, being careful not to walk onto any of the lions, or get too close to a napping lion, or be within the field of vision of an alert lion.

The lions do not move, and although they are several tiles wide, they will be facing only left or right (so we do not need “up” and “down” facing frames, and we can mirror the frames, so we really need only one set of “left” facing animations). Notice again that the sprite is wrapped at 1,024 pixels (see Figure 4-8).

A ferocious lion
Figure 4-8. A ferocious lion

Entity

The Lion class will inherit from Entity. We will define an enumeration that specifies all of the different AI states the lion can be in. We will also use a float to count the amount of time the lion has been in a state (so that we can decide when to switch from sleeping to napping, etc.) and an integer to keep track of how much the player has disturbed the lion’s sleep by being too close. Finally, the lions can be facing left or right, which we will keep track of with the flip bool (if flip is true, they are facing to the right):

typedef enum LionState {
    ASLEEP = 0,
    WAKING,
    ALERT,
    ATTACKING
} LionState;

@interface Lion : Entity {
    LionState state;
    float state_time;
    int m_rage;
    bool flip;
}

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr;
- (void) wakeAgainst:(Entity*) other;
- (void) obstruct:(TileWorld*)world;

@end

Notice the wakeAgainst: and obstruct functions. In each frame, we will check the distance between the player and the lion to see whether it should wake up, invoking the wakeAgainst: function if it should.

In addition, we want to make sure the player does not walk through the lions. If these were entities that could move about, that would require entity-to-entity collision detection between the player and all of the lions. However, since the lions are stationary, we can cheat: each time a lion is created, it will access the TileWorld to get the two tiles it is on top of, and set their physics flags to UNWALKABLE. That is what the obstruct function will do.

Let’s look at the implementation of this class. First, we have a utility function that gives us the AnimationSequence names that should be used for each state the lion can be in. If the lion is flipped we need to call the corresponding -flip sequences:

- (NSString*) animForState:(LionState) s {
    NSString* states[] = {
        @"asleep",
        @"waking",
        @"alert",
        @"attacking",
    };
    NSString* states_flip[] = {
        @"asleep-flip",
        @"waking-flip",
        @"alert-flip",
        @"attacking-flip",
    };
    return (flip?states_flip:states)[s];
}

Next, we have a useful function for switching states. Because we want to make sure to change not only the state variable but also the sprite animation, and additionally reset the state_time and rage counter whenever we change state, we tuck all of those operations into one tidy function:

- (void) setState:(LionState) s {
    state = s;
    sprite.sequence = [self animForState:s];
    state_time = 0;
    m_rage = 0;
}

The initialization function is short and sweet. We accept a new position, sprite, and facing, and set our initial AI state to ASLEEP. Remember that the setState: function sets our sprite sequence, state_time, and rage counters for us so that we don’t need to worry about initializing them:

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr facingLeft:(bool)faceleft
{
    [super initWithPos:pos sprite:spr];
    flip = !faceleft;
    [self setState:ASLEEP];
    return self;
}

The update function is the heart of our lion AI code. It begins by counting up the elapsed state_time. We then branch through a switch statement based on our current state. If we are asleep, we will periodically and randomly check to see whether we should wake up or keep sleeping. If we are waking, we will periodically and randomly decide to switch to the ALERT state, back to the ASLEEP state, or neither. If we are alert but our rage counter is empty, we will fall back to the WAKING state after two seconds have elapsed. And finally, if we are attacking but our rage counter is empty, we will return to the ALERT state after two seconds:

- (void) update:(CGFloat) time {
    state_time += time;
    switch (state) {
        case ASLEEP:
            //5 times a second, take a 2% chance to wake up.
            if(state_time > 0.2f){
                if(random() % 1000 < 20){
                    [self setState:WAKING];
                } else {
                    state_time = 0;
                }
            }
            break;
        case WAKING:
            if(state_time > 0.2f){
                if(random() % 1000 < 20){
                    [self setState:ALERT];
                } else if(random() % 1000 < 20){
                    //narcoleptic lions.
                    [self setState:ASLEEP];
                } else {
                    state_time = 0;
                }
            }
            break;
        case ALERT:
            if(state_time > 2.0f && m_rage == 0){
                [self setState:WAKING];
            
            }
            break;
        case ATTACKING:
            if(state_time > 2.0f && m_rage == 0){
                [self setState:ALERT];
            }
            break;
        default:
            break;
    }
    [sprite update:time];
}

But none of this code has anything to do with the player’s proximity to the lion yet. To check the proximity between the lion and the player, we need a pointer to the player. Therefore, we have another function that will be called on each loop (in addition to the update function) to perform that duty:

- (bool) wakeAgainst:(Entity*) other {
    //each state has a different triggering radius...
    // you can get closer to sleeping lions,
    // and should stay further from alert lions.
    float vision[] = {
        TILE_SIZE, TILE_SIZE*2, TILE_SIZE*3, TILE_SIZE*3,
    };
    float distSQ = distsquared(self.position, other.position);
    if(dist < vision[state]*vision[state]){
        m_rage++;
    } else {
        m_rage--;
        if(m_rage < 0) m_rage = 0;
    }

    //how much rage is needed to advance to the next rage state
    int ragemax[] = {
        1, 20, 20, 20,
    };

    if(m_rage > ragemax[state]){
        if(state == ATTACKING){
            m_rage = ragemax[state];
            //can't rage more.
        } else {
            [self setState:state+1];
        }
    }
    //attack hit detection against the player.
    if(state == ATTACKING){
        CGPoint attackoffset = CGPointMake(-TILE_SIZE, 0);
        if(flip){
            attackoffset.x = - attackoffset.x;
        
        }
        attackoffset = add(worldPos, attackoffset);
        float dist = distsquared(attackoffset, other.position);
        if(dist < 64*64){
            //kill player.
            return true;
        }
    }
    return false;
}

First, we have an array called vision with values that represent the distance at which the lion can sense the character’s presence, corresponding to which state the lion is in. When sleeping, he will be disturbed only by the character standing right next to him, but when he is alert, he will see the character from three squares away.

Next, we calculate the distance (squared) from the character and compare it to the appropriate vision level (again squared). If the character is in range, the rage counter increases; otherwise, it decreases. We need to be sure to cap it at 0 on the low end, but we don’t have to cap the high end here.

When the rage counter increases past a certain point, we will change states. That point is based on our current state, so just like vision, we will use another array of values to represent this. When the lion is ASLEEP, the player needs to be within range for only one update loop before the lion switches to WAKING. If the lion is WAKING, it will wait for the rage counter to reach 20 before switching to ALERT, and the same from ALERT to ATTACKING. If the lion is already ATTACKING, it has no greater recourse, so we will cap the rage counter.

If the lion is attacking, we need to check whether the player is within attack range. We check the player’s position against the front of the lion. We have to take the flip variable into account here, and modify the check position if the lion is flipped. When the player is within attack range, we simply return true from wakeAgainst; it will be up to the caller to actually kill the player.

Note

The values in vision and ragemax directly affect game play. If we were to increase the vision range of the lion or decrease the rage limits between states, we would make the game much harder for the player. Conversely, changing these values in the opposite direction would make it easier. These are the kinds of values game designers want to be able to play with to “tweak the game”; tuning them to get the feel just right.

In fact, if we wanted to add a “difficulty level” feature to allow the player to choose between playing the game in “easy mode” or “hard mode,” we could tweak these values one way or the other based on the difficulty the player selected.

And lastly, the obstruct: function simply grabs the two tiles the lion is hovering over and sets their flags to UNWALKABLE, like so:

- (void) obstruct:(TileWorld*) world {
    Tile* t = [world tileAt:worldPos]; //right square
    t->flags |= UNWALKABLE;
    //left square, so offset x by -TILE_SIZE
    t = [world tileAt:CGPointMake(worldPos.x-TILE_SIZE, worldPos.y)];
    t->flags |= UNWALKABLE;
}

gsLionLevel

Integrating the Lion class into our gsLionLevel requires that we modify the setupWorld: and update: functions.

In setupWorld:, we begin by hardcoding the position and direction of our lions. If we were reusing this state for multiple levels, it would be worth it to load these values from a resource file instead of hardcoding them. But we’re not, so we don’t.

Next, we allocate the Animation and an array of lion pointers. We then iterate that array and create and initialize each Lion entity similar to the way we did the Emu entities from the previous level, but with a twist. We are using the hardcoded positions and facings and are randomly selecting between the male and female lion textures when we initialize our Sprite for each lion.

Wrapping up, we add each Lion entity to the TileWorld for rendering purposes and then call the obstruct: function from before to make sure the player can’t walk over them:

int lion_positions[] = {
    2,11,
    4,13,
    4,14,
    5,11,
    5,9,
    6,12,
    8,14,
    8,8,
    9,13,
    9,9,
    10,12,
};
bool lion_faceleft[] = {
    false,
    true,
    true,
    false,
    true,
    false,
    true,
    true,
    false,
    true,
    true,
};
int lioncount = 11;

Animation* lionanim = [[Animation alloc] initWithAnim:@"lion.png"];
Lion** lions;
lions = malloc(lioncount*sizeof(Lion*));
for(int i=0;i<lioncount;i++) {
    Lion* otherlion = [[Lion alloc]
      initWithPos:
           CGPointMake((left+lion_positions[i*2+0])*TILE_SIZE,
                       (bottom+lion_positions[i*2+1])*TILE_SIZE)
      sprite:[Sprite spriteWithAnimation:random()%100<25?lionanim:lioness]
      facingLeft:lion_faceleft[i]];

    [tileWorld addEntity:otherlion];
    [otherlion obstruct:tileWorld];
    lions[i] = otherlion;
}
[lionanim autorelease];
m_lions = lions;
lions_length = lioncount;

The change to the update function is very simple; just call the wakeAgainst: and update: functions and we’re finished plugging in the lions:

for(int i=0;i<lions_length;i++){
    if([m_lions[i] wakeAgainst:m_tom]){
        [self onFail];
        [m_tom dieWithAnimation:@"dying"];
    }
    [m_lions[i] update:time];
}

Game Logic

Unlike the previous level, we have two end states in Level 2. If a lion swipes the main character, we will go to the level failed state. However, if the main character reaches the McGuffin and returns to the bottom of the level, we will advance to the level completed state.

First, we must determine when the player touches the McGuffin and remove it from the TileWorld render list to represent that the player has “picked it up.” Next, we determine when the player is back at the cave mouth and start celebrating.

You can find this code in the Update: function of the gsLionLevel class:

if(m_goal && distsquared(m_tom.position, m_goal.position) < 32*32){
    //grab the macguffin and rush for the door.
    [tileWorld removeEntity:m_goal];
    [m_goal release];
    m_goal = nil;
}
if(m_goal == nil && m_tom.position.y < 8*TILE_SIZE){
    //clear to the door, so win.
    m_tom.celebrating = true;

}

Sound

When the lion takes a swipe at the player, we want to play a sound effect. We will inject this code directly into the setState: function of the Lion class:

- (void) setState:(LionState) s {
    state = s;
    sprite.sequence = [self animForState:s];
    state_time = 0;
    m_rage = 0;
    if(state == ATTACKING){
        [g_ResManager playSound:@"tap.caf"];
    }
}

Level 3 Implementation

The third level is a puzzle. The main character is located in a room at the top of the level that contains three buttons. He will accomplish his goal or fail while staying in that room. The rest of the level consists of nine cells connected with gates that are controlled by the buttons in the upper room. Inside the cells are a mouse, a cat, and cheese. The cat is located in the upper-left cell, the mouse in the upper right, and the cheese in the bottom left.

The player must move the main character to press the buttons in a way that will open gates for the mouse, allowing it to reach the cheese while avoiding the cat. Each time the player opens a gate next to the cell occupied by the mouse, the mouse will move into the open cell. Afterward, the cat will move into a random cell as well. If the cat and mouse end up in the same cell, the cat will eat the mouse and the player will lose. If the mouse ends up in the cell with the cheese, the player will win.

This will require the following resources:

  • A tile level showing the upper room and the nine cells, divided by walls. The walls between each cell should include an open space because the gates will be entities that are drawn on top of the walls, not a part of the tile level.

  • The Tom sprite we used before.

  • The button entities, with unpressed and pressed frames. There will be three buttons, each a different color.

  • The gates (one vertically and one horizontally aligned), open and closed, with colored lights to indicate which buttons they will work with.

  • The mouse sprite.

  • The cat sprite.

  • The cheese sprite.

Since this level is a puzzle, the game logic here will be more complex than previous levels. There is still only one way to enter the win state (when the mouse reaches the cheese) and only one way to enter the lose state (when the cat captures the mouse), but we must also control the buttons and gates from the GameState level.

The AI logic will consist of the cat and the mouse. The mouse is simple: when a door opens, the mouse will move to the next cell. The cat will decide a random cell to move to and will open its own gate.

User input will be similar to Level 2, but now we will add a context-sensitive action. When the user touches the screen above the player’s avatar, we will check to see whether the touch is near a button; that context determines whether we react. Specifically, if the avatar is near a button, the resulting game change will be activated.

gsMazeLevel

Again, we will create a new GLESGameState class to represent our level:

#define buttons_length 3

@interface MazeLevel : GLESGameState {
    TileWorld* tileWorld;
    Tom* m_tom;
    Tom *cat, *mouse;
    Entity* cheese;
    MazeDoor* door[2][2][3];
    MazeButton* buttons[buttons_length];
    MazeState state;

    //todo; mcGuffin* cheese;
}

@end

As you can see, there are many types of entities in this level. As usual, we have Tom acting as the player’s avatar. We also have our trusty TileWorld behind the scene and the McGuffin entity (in this case, named cheese). Our new actors will be the CAT and MOUSE along with the MazeDoor and MazeButton.

There will be one CAT and one MOUSE. They will each start in different corners of the “maze” portion of the level, and they will be controlled directly by the game logic.

There will be three MazeButtons, all accessible to the player at the top of the level in the “control room” area. When the player performs a context-sensitive action, it will trigger game logic that sets off one of the buttons and the corresponding MazeDoor.

There are nine cells, and there is a MazeDoor between each cell.

TileWorld

Figure 4-9 shows the TileWorld for Level 3.

Level 3 mockup
Figure 4-9. Level 3 mockup

The level map doesn’t have any simple duplicated tiles, so it gets sliced up into the tile strip in Figure 4-10.

Level 3 tiles
Figure 4-10. Level 3 tiles

Here is the lvl3_idx.txt tile index and physics information (“1” represents impassable and “0” represents normal tiles):

10x15

0,1,2,3,4,5,6,7,8,9
10,11,12,13,14,15,16,17,18,19
20,21,22,23,24,25,26,27,28,29
30,31,32,33,34,35,36,37,38,39
40,41,42,43,44,45,46,47,48,49
50,51,52,53,54,55,56,57,58,59
60,61,62,63,64,65,66,67,68,69
70,71,72,73,74,75,76,77,78,79
80,81,82,83,84,85,86,87,88,89
90,91,92,93,94,95,96,97,98,99
100,101,102,103,104,105,106,107,108,109
110,111,112,113,114,115,116,117,118,119
120,121,122,123,124,125,126,127,128,129
130,131,132,133,134,135,136,137,138,139
140,141,142,143,144,145,146,147,148,149

1,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,1
1,0,0,0,0,0,0,0,0,1
1,1,1,1,1,1,1,1,1,1
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0

Notice that the physics flags define only the control room area the player will be bounded by. There is nothing to represent the cell walls that the cat and mouse must stay in. This is because their positions will be entirely defined by game logic; we will not need entity-to-world collision detection for them.

Buttons

The MazeButton entity itself is very simple. It merely shows an on or off state. It will render the states using the appropriate color for each button, which are all placed in one sprite for the usual efficiency in image loading. Because the buttons will be the target of a context-sensitive action, we will also need a way to determine whether each button is eligible for an action based on its proximity to the player’s avatar:

//MazeButton.h
@interface MazeButton : Entity {
    int color;
}

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr color:(int)col;
//used to trigger the pressed animation.
- (void) press;
//used to determine if this button is under the specified entity
- (bool) under:(Entity*)other;

@end

The integer will store which color the button represents; this will be useful both for displaying the appropriate sprite and for letting the game logic know which button was pressed during a context-sensitive action:

MazeButton.m
@implementation MazeButton

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr color:(int)col {
    [super initWithPos:pos sprite:spr];
    color = col;
    [self press]; //probably should start with up image.  using down for now...
    return self;
}

static const NSString* down[] = {
        @"down",
        @"down-green",
        @"down-red",
    };


- (void) press {

    sprite.sequence = down[color];
}

- (bool) under:(Entity*)other {
    return distsquared(worldPos, other.position) < TILE_SIZE*TILE_SIZE;
}

@end

Initialization consists of storing the color and calling the press function.

The press function sets the sprite to render the appropriate color animation. Based on the sprite data in the .plist for buttons.png, the “down” animation will automatically reset to the “up” animation once it is complete.

Finally, the under: function will return true if the player is within one tile of the button when the function is called.

Doors

Similar to the MazeButton class, MazeDoors will also be very simple:

@interface MazeDoor : Entity {
}

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr ;

@end

This entity holds the sprite that represents the door at various states of being opened, closed, opening, closing, and closed with a colored light. However, since the door entity itself does not control these states (rather, the game logic will), we do not need special code in this class beyond initialization:

- (id) initWithPos:(CGPoint) pos sprite:(Sprite*)spr
{
    [super initWithPos:pos sprite:spr];
    sprite.sequence = @"closed";
    return self;
}

The initialization function simply sets the first animation of the door as closed and leaves the rest to our game logic.

Cat and Mouse

The cat and mouse entities reuse the Tom class because the animation sequences are the same. Meanwhile, they have no direct AI. The AI is located in the game logic and merely commands the entities in the same way as user input commands Tom.

We could have named the Tom class the “externally controlled avatar” to make it more general. But that just sounds silly.

User Input

This level’s context-sensitive action will take place inside the touchesEnded: function. This time, instead of immediately calling moveToPosition on Tom when we receive a single tap, we will check the distance between the tap and each button. If it is within a certain distance, we will interpret the input as a command to activate the action associated with the button. Therefore, we search on the three buttons and perform the appropriate game logic if we find one near enough to the touch:

- (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch* touch = [touches anyObject];
    CGPoint touchpos = [tileWorld worldPosition:[self touchPosition:touch]];
    float dist = distsquared(touchpos, m_tom.position);
    if(dist < 30*30){
        if(state != WAITING_FOR_PLAYER) return;
        //figure out if the player has stomped a button.
        int buttonpressed = −1;
        for(int i=0;i<buttons_length;i++){
            if([buttons[i] under:m_tom]){
                [buttons[i] press];
                buttonpressed = i;
                break;
            }
        }
        if(buttonpressed == −1) return;

/// code snipped - performing game logic

        }
    } else {
        [m_tom moveToPosition:touchpos];
    }
}

Game Logic

Most of the code in this level is going to be about game logic. In fact, apart from the random relocation of the cat entity, there is no AI. The game proceeds purely on user input and the rules of the maze game.

To begin, we’ll explicitly list all of the states the maze game can be in:

typedef enum MazeState {
    MOVING_MOUSE=0,
    MOVING_CAT,
    WAITING_FOR_PLAYER,
    GOT_CHEESE, //winning state
    MOUSE_KILLED, //losing state
} MazeState;

We start in the WAITING_FOR_PLAYER state as we are waiting for the player to walk over to a button and activate it. Once the player has made her selection, we will enter the MOVING_MOUSE state as we open the corresponding door and move the mouse into the next cell.

When that is complete, we must determine whether the mouse has reached the cheese, in which case, we can move to the GOT_CHEESE state.

Otherwise, the game will automatically advance to the MOVING_CAT state, where we will randomly open a door and move the cat into a new cell.

Lastly, we determine whether the cat has entered the same cell as the mouse, and if so, we enter the MOUSE_KILLED state; otherwise, we return to the WAITING_FOR_PLAYER state and the cycle begins anew.

Initialization

Armed with this simple state machine, we can initialize our game. As usual, we place our initialization code into the setupWorld: function.

We begin by allocating the TileWorld. Because this level was designed to fit the screen exactly, without the need to scroll, we can set the camera to (0, 0) and not have to update it again.

Next, we allocate the main character as usual, placing him in the top control room area:

- (void) setupWorld {
    tileWorld = [[TileWorld alloc] initWithFrame:self.frame];
    [tileWorld loadLevel:@"lvl3_idx.txt" withTiles:@"lvl3_tiles.png"];
    [tileWorld setCamera:CGPointMake(0, 0)];

    Animation* tomanim = [[Animation alloc] initWithAnim:@"tom_walk.png"];
    m_tom = [[Tom alloc] initWithPos:CGPointMake(5.0f*TILE_SIZE, 12*TILE_SIZE)
                         sprite:[Sprite spriteWithAnimation:tomanim]];
    [tileWorld addEntity:m_tom];
    [tomanim autorelease];

The main character is followed by the cat, mouse, and McGuffin entities:

    Animation* catanim = [[Animation alloc] initWithAnim:@"cat.png"];
    cat = [[Tom alloc] initWithPos:CGPointMake(2*TILE_SIZE, 8*TILE_SIZE)
                       sprite:[Sprite spriteWithAnimation:catanim]];
    [tileWorld addEntity:cat];
    [catanim release];

    Animation* mouseanim = [[Animation alloc] initWithAnim:@"mouse.png"];
    mouse = [[Tom alloc] initWithPos:CGPointMake(8*TILE_SIZE, 8*TILE_SIZE)
                         sprite:[Sprite spriteWithAnimation:mouseanim]];
    [tileWorld addEntity:mouse];
    [mouseanim autorelease];


    Animation* goalanim = [[Animation alloc] initWithAnim:@"mcguffin.png"];
    cheese = [[Entity alloc]
                initWithPos:CGPointMake(2*TILE_SIZE, 2*TILE_SIZE+0.1f)
                sprite:[Sprite spriteWithAnimation:goalanim]];
    [tileWorld addEntity:cheese];
    [goalanim autorelease];

Now we begin adding the cell doors. As you may notice from the level mockup, the cells are all drawn without doors. This lets us overlay the door sprites onto the scene, allowing them to show the unobstructed passages underneath when they are opened.

There are nine cells, with a door between every two adjacent cells. That means two rows of three vertically aligned doors, and two columns of three horizontally aligned doors.

The central square has one door that doesn’t open. We’ve made this compromise in the design so that we need only three buttons; it’s not worth adding a fourth button just so that one cell can have a fourth door:

    Animation* vertdoor = [[Animation alloc] initWithAnim:@"mazedoor.png"];
    for(int x=0;x<2;x++){
        for(int y=0;y<3;y++) {
            door[0][x][y] =
                [[MazeDoor alloc] initWithPos:
                    CGPointMake((3*x+3.5f)*TILE_SIZE, (3*y+1.0f)*TILE_SIZE)
                                sprite:[Sprite spriteWithAnimation:vertdoor]];
            [tileWorld addEntity:door[0][x][y]];
        }
    }
    [vertdoor autorelease];

    Animation* horizdoor = [[Animation alloc] initWithAnim:
                                               @"mazedoor-horizontal.png"];
    for(int x=0;x<3;x++){
        for(int y=0;y<2;y++) {
            door[1][y][x] =
                [[MazeDoor alloc] initWithPos:
                    CGPointMake((3*x+2.0f)*TILE_SIZE, (3*y+3.5f)*TILE_SIZE)
                               sprite:[Sprite spriteWithAnimation:horizdoor]];
            [tileWorld addEntity:door[1][y][x]];
        }
    }
    [horizdoor autorelease];

Three buttons are located within the control room area. They need to be spaced out enough so that only one button is ever eligible for a context-sensitive action at any given time:

    Animation* buttonanim = [[Animation alloc] initWithAnim:@"buttons.png"];
    for(int i=0;i<buttons_length;i++){
        buttons[i] =
              [[MazeButton alloc] initWithPos:
                  CGPointMake((2.5f+2.5f*i)*TILE_SIZE,12*TILE_SIZE)
                     sprite:[Sprite spriteWithAnimation:buttonanim] color:i];
        [tileWorld addEntity:buttons[i]];
    }
    [buttonanim release];

Finally, because we are starting in the WAITING_FOR_PLAYER state, we need to show colored lights above the doors leading out of the mouse’s current cell. We are going to have to perform this action a lot, so let’s create a function named decorateDoors to handle this feature:

    state = WAITING_FOR_PLAYER;
    [self decorateDoors];
}

Context action

We already showed the code in touchesEnded that detects when players have performed a context-sensitive action; now we will examine the game logic that gets triggered when they do:

if(state != WAITING_FOR_PLAYER) return;
//figure out if the player has stomped a button.
int buttonpressed = −1;
for(int i=0;i<buttons_length;i++){
    if([buttons[i] under:m_tom]){
        [buttons[i] press];
        buttonpressed = i;
        break;
    }
}
if(buttonpressed == −1) return;

//perform action.
NSArray* moves;
int angle;

moves = [self possibleMoves:mouse];
if(buttonpressed < [moves count]){
    angle = [[moves objectAtIndex:buttonpressed] intValue];
    //open the chosen door
    [self doorFrom:mouse.position inDirection:angle].sprite.sequence = @"opening";

    //un-decorate the other possible doors.
    for(NSNumber* n in moves){
        int i = [n intValue];
        if (i==angle) continue;
        [self doorFrom:mouse.position inDirection:i].sprite.sequence = @"closed";
    }

    //move the char through the open door.
    CGPoint mousepos = mouse.position;
    //int angle = random() % 4;
    mousepos.y += TILE_SIZE*3*cheapsin[angle];
    mousepos.x += TILE_SIZE*3*cheapcos[angle];
    [mouse moveToPosition:mousepos];
    //wait for the mouse to stop moving.
    state = MOVING_MOUSE;
}

After determining which button the player pressed, we call the press function on that button to show graphically that the button has been pressed.

Remember, each cell has either two or three doors. (One wall in the central cell has a door that doesn’t open, as we explained earlier.) That means corner cells, which have only two adjacent cells, will have only two possible moves, whereas the side and center cells will have three.

After retrieving the list of possible moves the player could have made, we check to see whether the button that was pressed corresponds to one of those moves. If the mouse is in a corner cell and the third button is pressed, it would fail this test and we would not open any doors. If you look ahead, you will see that the state is set to MOVING_MOUSE only if this test passed. That means the player can press the wrong button and see it depressed, but no door will open and she will still be able to choose a different button.

Next, we retrieve the direction of the door from the moves array and use it to open the corresponding door. Retrieving the right door is slightly complicated, so we place it inside a subfunction that can return a door based on a cell and a direction from that cell:

- (MazeDoor*) doorFrom:(CGPoint) pos inDirection:(int) angle {
    int x = gamepos(pos.x);
    int y = gamepos(pos.y);
    if(angle == 1 || angle == 3){
        //moving left/right will find horizontal column doors only
        if(angle == 1) x--;
        return door[0][x][y]; //3 columns of 2 rows
    } else {
        //moving up/down will find verticle row doors only
        if(angle == 2) y--;
        return door[1][y][x];
        //x and y are reversed for horizontal doors,
        //so that we can squeeze 2x3 of them in.
    }
}

Once we have a door opening, we should remove the colored lights from the other possible doors by setting their animations to “closed”.

Finally, we determine the new mouse position and send it to the mouse actor, similar to the way we send the main character to a position when the player taps the screen.

Update

In the Update: function, we use a switch statement based on our current MazeState to perform the appropriate logic:

//clockwise from straight up, in opengl coords.
int cheapsin[] = { 1, 0, −1, 0 };
int cheapcos[] = { 0, −1, 0, 1 };

- (void) Update {
    float time = 0.033f;

    //move mouse, wait to stop.
    //move cat, wait to stop.
    //decorate doors, wait for button push.
    switch (state) {
        case MOVING_MOUSE:

In the MOVING_MOUSE state, we will be waiting for the mouse to finish moving (it was already told to begin moving at the start of the state when the player pressed one of the buttons). Once the mouse has reached its destination cell, we will check to see whether it found the cheese and move to the GOT_CHEESE state if it has.

Otherwise, we need to start moving the cat. We list the possible directions in which the cat can move and randomly select one. We then open the corresponding door and walk through it, just like we told the mouse to do in the touchesEnded: function:

            if([mouse doneMoving]){
                if (distsquared(mouse.position, cheese.position) < 16) {
                    //todo: win.
                    state = GOT_CHEESE;
                    NSLog(@"win condition triggered.");
                } else {
                    //time to move the cat.
                    NSArray* moves = [self possibleMoves:cat];
                    int angle = [[moves
                            objectAtIndex:random()%[moves count]] intValue];

                    MazeDoor* door_tmp = [self doorFrom:cat.position
                                    inDirection:angle];

                    door_tmp.sprite.sequence = @"opening";

                    //move the char through the open door.
                    CGPoint catpos = cat.position;
                    //int angle = random() % 4;
                    catpos.y += TILE_SIZE*3*cheapsin[angle];
                    catpos.x += TILE_SIZE*3*cheapcos[angle];
                    [cat moveToPosition:catpos];
                    state = MOVING_CAT;
                }
            }
            break;
        case MOVING_CAT:

In the MOVING_CAT state, we are waiting for the cat to finish walking to its new cell. Once it has, we should determine whether the cat caught the mouse, in which case, we move to the MOUSE_KILLED state; otherwise, we return to WAITING_FOR_PLAYER:

            if([cat doneMoving]){
                if(distsquared(mouse.position, cat.position) < 16){
                    //todo: lose.
                    state = MOUSE_KILLED;
                    NSLog(@"lose condition triggered.");
                } else {
                    [self decorateDoors];
                    state = WAITING_FOR_PLAYER;
                }
            }
            break;
        default:

The rest of the states do not require special processing in the update function. WAITING_FOR_PLAYER is just waiting for the player to activate a button, and the GOT_CHEESE and MOUSE_KILLED states are our win and lose states:

            //nothing interesting here...
            break;
    }

    [m_tom update:time];
    [cat update:time];
    [mouse update:time];
    for(int i=0;i<2;i++){
        for(int x=0;x<2;x++){
            for(int y=0;y<3;y++){
                [door[i][x][y] update:time];
            }
        }
    }
    for(int i=0;i<buttons_length;i++){
        [buttons[i] update:time];
    }
    //[tileWorld setCamera:[m_tom position]]; //unnecessary for this level
}

Finally, we make sure to update all of our entities so that they can animate their sprites.

Sounds

When the player presses a button, we want to play a click sound effect. We will add the code into the press: function of the MazeButton class:

static const NSString* down[] = {
        @"down",
        @"down-green",
        @"down-red",
    };


- (void) press {

    sprite.sequence = down[color];
    [g_ResManager playSound:@"tap.caf"];
}

Level 4 Implementation

Level 4 features the timing of jumps across log platforms while avoiding crocodile enemies. Like before, we will be focusing on context-sensitive actions (the jump action will require the same user input as pressing buttons did in the previous level), but this time we use it to cross special water tiles.

This level will require the following sprites:

  • A tile level consisting of a series of solid ground patches separated by water tiles

  • The same Tom sprite as before, adding the “jump” animation

  • A log sprite with “floating” and “sinking” animations

  • A crocodile sprite with “swimming” and “attacking” animations

  • A plant sprite to make the level look like an overgrown swamp

The game logic will include detecting loss and win states. The two ways to lose in this level are to fall into water or to get eaten by a crocodile. The way to win is to reach the end of the level and grab the McGuffin.

The AI will consist of logs that sink when the character lands on them and crocodiles that jump out of the water and snap at the character if the character jumps over them.

We will need to update the Tom class to support the new jump behavior. He is allowed to jump upward (in the positive direction on the y-axis) as long as either solid ground or a log is within three tiles in that direction.

User input will be the same as in the preceding level, except that instead of searching for buttons, we will search for the edge of a platform or a log to jump to.

gsRiverLevel

As in the previous levels, we will create a new class that inherits from GLESGameState:

@interface RiverLevel : GLESGameState {
    TileWorld* tileWorld;
    Tom* m_tom;
    Rideable* log[log_length];
    Croc *croc[croc_length];
    Entity* m_goal;
    int state;
}

This level reuses the TileWorld, Tom, and Entity classes from earlier. We also introduce the Rideable class for floating log platforms and Croc to represent the crocodiles.

TileWorld

The tile world should look like Figure 4-11.

Level 4 mockup
Figure 4-11. Level 4 mockup

Figure 4-12 shows the unique tiles.

Level 4 tiles
Figure 4-12. Level 4 tiles

And here is the lvl4_idx.txt file:

12x30
11,11,11,11,11,11,11,11,11,11,11,11
16,17,5,6,6,6,6,6,6,7,11,11
11,11,3,3,3,3,3,3,3,3,11,12
11,11,9,9,9,9,9,9,9,9,16,11
11,11,11,11,11,11,11,11,11,11,11,11
6,6,6,6,6,7,11,11,11,11,11,11
3,3,3,3,3,3,11,5,6,6,6,6
9,9,9,9,9,9,11,3,3,3,3,3
11,11,11,11,11,11,11,9,9,9,9,9
11,12,11,11,11,11,11,11,11,11,11,11
11,13,5,6,6,6,6,6,7,15,11,11
11,2,1,1,1,1,1,1,1,4,11,17
11,8,3,3,3,3,3,3,3,10,11,11
11,11,9,9,9,9,9,9,9,11,16,17
11,11,11,11,11,11,11,11,11,11,17,11
16,11,11,11,11,11,11,11,11,11,11,11
11,11,11,11,11,12,11,11,11,11,11,11
11,11,11,12,11,11,11,11,11,12,11,11
11,11,11,11,11,11,11,11,11,12,11,11
6,6,6,6,6,6,6,6,6,6,6,6
1,1,1,1,1,1,1,1,1,1,1,1
3,3,3,3,3,3,3,3,3,3,3,3
9,9,9,9,9,9,9,9,9,9,9,9
11,11,11,11,11,11,11,11,11,11,11,11
17,11,12,11,11,11,11,11,11,11,16,17
11,16,12,11,11,11,11,11,11,12,11,11
6,6,6,6,6,6,6,6,6,6,6,6
1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1
1,1,1,1,1,1,1,1,1,1,1,1

3,3,3,3,3,3,3,3,3,3,3,3
3,3,0,0,0,0,0,0,0,0,3,3
3,3,0,0,0,0,0,0,0,0,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
0,0,0,0,0,0,3,3,3,3,3,3
3,3,3,3,3,3,3,0,0,0,0,0
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,0,0,0,0,0,0,0,3,3,3
3,0,0,0,0,0,0,0,0,0,3,3
3,3,0,0,0,0,0,0,0,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
3,3,3,3,3,3,3,3,3,3,3,3
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0

In this level, we use the physics flag 3 to represent water tiles.

Crocodiles

The crocodile includes three frames of swimming, three frames of beginning the attack, and six frames of jumping out of the water to snap at the player’s character. This is unrealistic behavior for a crocodile, but at the risk of misrepresenting nature, it gives the player an exciting explanation for why she just lost.

The definition of the Croc class includes an integer that tracks the direction of the crocodile (it flips between 1 and –1 as the crocodile turns around at the edge of the level) and a CGRect representing the bounds of the croc that the player must avoid:

@interface Croc : Entity {
    int direction; //-1 or 1, for pacing back and forth across the level.
    CGRect bounds; //used for hit detection on jumping player.
}

- (bool) under:(CGPoint)point;
- (void) attack:(Entity*) other;

@end

The interesting functions in the Croc class include under:, attack:, and update:. The under: function will return true if the given point collides with the crocodile’s bounds (offset by its current position):

- (bool) under:(CGPoint)point {
    if(
        point.x > self.position.x + bounds.origin.x &&
        point.x < self.position.x + bounds.origin.x + bounds.size.width &&
        point.y > self.position.y + bounds.origin.y &&
        point.y < self.position.y + bounds.origin.y + bounds.size.height
        ) return true;
    return false;
}

The attack: function is used when the crocodile is found underneath the player’s character. We are using the current sprite to represent the AI state of the Croc. Only if it is in the “idle” or “idle-flip” animation is the crocodile ready to attack. If it is, we snap its position to be directly underneath the player’s character and then start the attacking animation:

- (void) attack:(Entity*) other {
    if([sprite.sequence isEqualToString:@"idle"] ||
       [sprite.sequence isEqualToString:@"idle-flip"])
    {
        worldPos.x = other.position.x;
        sprite.sequence = direction==1?@"attack-flip":@"attack";
    }
}

The update: function simply moves the crocodile to the left or right, depending on its direction, being sure to flip around if it reaches either side of the world:

- (void) update:(CGFloat) time {
    [super update:time];
    if([sprite.sequence isEqualToString:@"idle"] ||
       [sprite.sequence isEqualToString:@"idle-flip"])
    {
        //pace back and forth.
        float speed = time*200; //in pixels per second.
        float nextx = speed*direction + worldPos.x;
        if(nextx < 0 || nextx > world.world_width*TILE_SIZE){
            direction = -direction;
            //-1 is @"idle", 1 is @"idle-flip"
            sprite.sequence = direction==1?@"idle-flip":@"idle";
        } else {
            worldPos.x = nextx;
        }
    }
}

Logs

The Rideable class is like the Croc class, but it is even simpler as it doesn’t move about:

@interface Rideable : Entity {
    CGRect bounds;
}

- (bool) under:(CGPoint) point;
- (void) markRidden:(Entity*) rider;

@end

Just like the Croc class, the Rideable class keeps track of a physics boundary. The under: function here is the same as Croc’s.

The markRidden: function will cause the log to start sinking if a pointer to an entity is passed or to begin rising if the entity is null:

- (void) markRidden:(Entity*) rider {
    if(rider){
        sprite.sequence = @"sinking";
    } else {
        sprite.sequence = @"rising";
    }
}

Tom

For this level, we will add new functionality to the Tom class—two new member variables and two functions to the header file:

//Tom.h
@interface Tom : Entity {
    CGPoint destPos; //tom-specific
    bool dying;

    bool inJump;
    Rideable* riding;
}

//... code snipped

- (void) jump;
@property (nonatomic, readonly) bool inJump;

The inJump bool will be true when we are currently jumping, false otherwise. The game logic can use this during the physics simulation to know whether we should check for collision with water.

The new Rideable pointer will be empty most of the time, but when the player is on a log, it will point to that entity. This will allow us to walk to the left or right while on a Rideable platform.

First, we modify the walkable: function to consider whether we are jumping, walking, or riding:

- (bool) walkable:(CGPoint) point {
    if(inJump) return true;
    if(riding == nil) return [world walkable:point];
    else return [riding under:point];
}

We also modify the moveToPosition: function to prevent new destinations from being input while in the middle of a jump action:

- (void) moveToPosition:(CGPoint) point {
    if(inJump) return; //freeze input while already jumping.
    destPos = point;
}

Next, we add a check in our update: function just above NSString* facing = nil; to handle the transition from jumping to landing on a platform:

if(inJump){
    if([self doneMoving]){
        [riding markRidden:self];
        inJump = false;
    }
}

And finally, we implement the jump function itself:

- (void) jump {
    if(inJump) return; //freeze input while already jumping.
    if(![world walkable:worldPos]){
        //search for a walkable tile in the +y direction to jump to.
        //   take the first within 2 tiles.
        for(int i=1;i<=2;i++){
           if([world walkable:CGPointMake(worldPos.x, worldPos.y+i*TILE_SIZE)])
            {
                inJump = true;
                destPos = CGPointMake(worldPos.x, worldPos.y+i*TILE_SIZE);
                if(riding){
                    [riding markRidden:nil];
                    riding = nil;
                }
                return;
            }
        }
    }

    NSArray* nearby = [world entitiesNear:worldPos withRadius:4*TILE_SIZE];
    for(Rideable* e in nearby){
        if(e == riding) continue; //ignore the log we are already on.
        //ignore things we can't ride on.
        if(![e isKindOfClass:[Rideable class]]) continue;
        CGPoint pos = CGPointMake(worldPos.x, e.position.y-1);
        if(![e under:pos]) {
            //NSLog(@"not lined up to make jump.");
            continue;
        }
        float dy = e.position.y - worldPos.y;
        if(
            dy > TILE_SIZE*3 //too far to jump
            ||
            dy < 0 //ignore logs behind us, only jump forward.
        ){
            //NSLog(@"too far to jump");
            continue;
        }
        inJump = true;
        if(riding){
            [riding markRidden:nil];
            riding = nil;
        }
        riding = e;

        destPos = pos;
        return;
    }
    NSLog(@"jump failed to initiate");
}

First, we make sure the jump function reacts only if we are not already jumping. Next, we search for a jump position. The current implementation of jump allows movement only in the up direction, so we only need to search upward (otherwise, we would search based on the direction we are currently facing).

First, we search upward for a passable tile to land on. If we find one, we set it as our destination position, turn on our inJump flag, and if we are currently attached to a Rideable platform, we detach it and return from our function.

If we didn’t find solid ground to land on, we begin searching for Rideable entities. From within the jump function, we don’t have access to RiverLevel’s Log array, but we do have a pointer to the TileWorld with which we can call entitiesNear: to retrieve a list of entities within a given radius of the player.

To weed out the entities that are not eligible platforms, we use the isKindOfClass function on each entity to determine whether they are instances of Rideable.

If they are Rideable, we project a vertical line to determine whether jumping straight up would land on the log. If not, we continue our search. Otherwise, we tune our search further and determine whether the object is within jumping distance (three tiles).

If the object is too far, we continue our search. If it’s within range, we mark our avatar as jumping, set our new destination, detach from the previous platform (if any), and attach to the new platform.

User Input

For this level, we want the context-sensitive action to call the new jump function on Tom:

-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch* touch = [touches anyObject];
    CGPoint touchpos = [tileWorld worldPosition:[self touchPosition:touch]];
    float dist = distsquared(touchpos, m_tom.position);
    if(dist < 30*30){
        [m_tom jump];
    }
    if(![m_tom inJump])
    {
        [m_tom moveToPosition:[tileWorld worldPosition:
                              [self touchPosition:touch]]];
    }
}

Notice that rather than simply using an if-else pair here, we check to see whether Tom is inJump after telling him to jump. This is because calling jump does not always result in a jump. If Tom is not in a place where he can jump, he won’t. In those cases, we still want to allow the player to move even small amounts (otherwise, we could wind up in a situation where Tom is two pixels away from a place he can jump from, but the player can’t get him to move there because the code is trying to interpret the move request as a jump action).

Game Logic

This level’s game logic will be heavily focused on physics. If Tom finds himself standing on a water tile, he will fall and drown; the player loses the level. This can occur only when standing on a log for too long because our jump function doesn’t allow the player to jump into the water. Tom faces another hazard while jumping across water, however: if he jumps over a croc, he will get chomped and lose the level.

Furthermore, while the character is jumping, we should avoid collision tests against the level for the water flag. Also, if the character has landed, we still need to check collisions for Rideable platforms before we decide he has collided with water.

Initialization

Our initialization continues as usual with the setup of TileWorld and our various entities (with hardcoded positions) inside the setupWorld function:

- (void) setupWorld {
    tileWorld = [[TileWorld alloc] initWithFrame:self.frame];
    [tileWorld loadLevel:@"lvl4_idx.txt" withTiles:@"lvl4_tiles.png"];

    Animation* tomanim = [[Animation alloc] initWithAnim:@"tom_walk.png"];
    m_tom = [[Tom alloc] initWithPos:CGPointMake(100, 100)
                        sprite:[Sprite spriteWithAnimation:tomanim]];
    [tileWorld addEntity:m_tom];
    [tomanim autorelease];

    int log_position[log_length*2] = {
        4,5,
        3,7,
        5,13,
        2,15,
        8,15,
        7,16,
        4,21,
        9,22,
        2,23,
        7,26,
    };

    Animation* loganim = [[Animation alloc] initWithAnim:@"log.png"];
    for(int i=0;i<log_length;i++){
        log[i] = [[Rideable alloc] initWithPos:
                        CGPointMake(
                            log_position[i*2+0]*TILE_SIZE,
                            log_position[i*2+1]*TILE_SIZE )
                        sprite:[Sprite spriteWithAnimation:loganim]];
        [tileWorld addEntity:log[i]];
        log[i].sprite.sequence = @"idle";
    }
    [loganim autorelease];


    int croc_position[croc_length*2] = {
        8,5,
        5,13,
        9,16,
        4,21,
        10,26,
    };

    Animation* crocanim = [[Animation alloc] initWithAnim:@"croc.png"];
    for(int i=0;i<croc_length;i++){
        croc[i] = [[Croc alloc] initWithPos:
                        CGPointMake(
                            croc_position[i*2+0]*TILE_SIZE,
                            croc_position[i*2+1]*TILE_SIZE+11)
                        sprite:[Sprite spriteWithAnimation:crocanim]];
        [tileWorld addEntity:croc[i]];
    }
    [crocanim autorelease];

    int bush_position[] = {
        16,16,
        16,112,
        16,272,
        16,304,
        16,336,
        16,784,
        48,592,
        304,592,
        304,912,
        320,752,
        368,48,
        368,272,
        368,304,
        368,336,
    };
    int bush_count=14;

    Animation* bushanim = [[Animation alloc] initWithAnim:@"plant.png"];
    for(int i=0;i<bush_count;i++){
        Entity* bush = [[Entity alloc] initWithPos:
                            CGPointMake(
                                bush_position[i*2+0],
                                bush_position[i*2+1])
                            sprite:[Sprite spriteWithAnimation:bushanim]];
        [tileWorld addEntity:[bush autorelease]];
    }
    [bushanim release];

    Animation* goalanim = [[Animation alloc] initWithAnim:@"mcguffin.png"];
    m_goal = [[Entity alloc] initWithPos:CGPointMake(192,896)
                        sprite:[Sprite spriteWithAnimation:goalanim]];
    [tileWorld addEntity:m_goal];
    [goalanim autorelease];

    [tileWorld setCamera:m_tom.position];

}

If you’ve studied the previous three levels, this code should be familiar.

Update

Now we need to update the player and the logs to determine whether the player’s character has fallen in the water. We also need to check the crocodiles to determine whether the character has been attacked and the McGuffin to determine whether the player has completed the level successfully:

- (void) Update {
    float time = 0.033f;
    if(endgame_state != LOSING)
        [m_tom update:time];
    for(int i=0;i<log_length;i++){
        [log[i] update:time];
    }

    for(int i=0;i<croc_length;i++){
        [croc[i] update:time];
        if(m_tom.inJump && [croc[i] under:m_tom.position]){
            [croc[i] attack:m_tom];
            //NSLog(@"lose, crikey!");
            [m_tom dieWithAnimation:@"dying"];
            [super onFail];
        }
    }

    if(m_tom.riding && [m_tom.riding.sprite.sequence isEqualToString:@"sunk"])
    {
        [m_tom dieWithAnimation:@"dying"];
        [super onFail];
        //NSLog(@"lose");
    }

    if(m_goal && distsquared(m_tom.position, m_goal.position) < 32*32){
        [super onWin];
        //NSLog(@"win");
    }

    [m_goal update:time];

    [tileWorld setCamera:[m_tom position]];
}

This code ends by updating the camera.

Sounds

When the player jumps, we want to play a “boing” sound effect. We add the code into the jump function of the Tom class:

- (void) jump {
    if(inJump) return; //freeze input while already jumping.
    if(![world walkable:worldPos]){
        for(int i=1;i<=2;i++){
           if([world walkable:CGPointMake(worldPos.x, worldPos.y+i*TILE_SIZE)])
           {
                inJump = true;
                [g_ResManager playSound:@"tap.caf"];
                destPos = CGPointMake(worldPos.x, worldPos.y+i*TILE_SIZE);
                //worldPos = desPos;
                if(riding){
                    [riding markRidden:nil];
                    riding = nil;
                }
                return;
            }
        }
    }

    //... code snipped
}

Game State Serialization

Now that we have implemented our levels, we need a way to show the progression between them and a way to jump between levels and the menu. This presents us with three tasks:

  • We need to store an integer value that represents how many levels have been unlocked.

  • When the player completes a level for the first time, we need to increment that value.

  • From our Main menu, we list all of the playable levels; we want to gray out the links to levels that are currently locked.

Initialize Storage

We begin by adding the integer to our persistent storage in the Game2DAppDelegate class. Therefore, we will add the following to our applicationDidFinishLaunching: function:

//Game2DAppDelegate.m
//setup default save data.
if([g_ResManager getUserData:ProgressionSavefile] == nil){
    //progression will store the unlocked level count, from 0..3.
    [g_ResManager storeUserData:[NSNumber numberWithInt:0]
                toFile:ProgressionSaveFile];
}

where ProgressionSaveFile is defined as:

#define ProgressionSaveFile @"progression"

End Game

Within our GLESGameStates, we have already written code to determine win and loss states. Now we will write some functions that all of our levels can use to handle user input and rendering for the end states (win or lose) and increment the progression integer.

We modify the GLESGameState class:

@interface GLESGameState : GameState {
    int endgame_state;
@private
    float endgame_complete_time;
}

//... code snipped

#define ENDGAME_GAME_IN_PROGRESS 0
#define ENDGAME_WINNING 1
#define ENDGAME_LOSING 2
#define ENDGAME_HOLD_TIME 2.0f

- (void) onWin:(int)level;
- (void) onFail;
- (void) renderEndgame;
- (void) updateEndgame:(float)time;
- (void) touchEndgame;

@end

We want to keep track of whether the GameState is still in progress, or has completed with a win or loss condition. We will also keep a time counter so that we can automatically return to the Main menu after displaying the win or loss result for a number of seconds as defined by ENDGAME_HOLD_TIME.

First, we must initialize our new variables:

-(id) initWithFrame:(CGRect)frame andManager:(GameStateManager*)pManager;
{
    if (self = [super initWithFrame:frame andManager:pManager]) {
        // Initialization code
        [self bindLayer];
        endgame_state = ENDGAME_GAME_IN_PROGRESS;
        endgame_complete_time = 0;
    }
    return self;
}

Next, we implement the onWin function, where we actually try to increment the progression variable (making sure to do so only if the level has not yet been completed):

- (void) onWin:(int)level{
    if(endgame_state == ENDGAME_GAME_IN_PROGRESS){
        int unlocked = [[g_ResManager
                            getUserData:ProgressionSavefile] intValue];
        if(level >= unlocked){
            //when we beat a level, unlock the next level.
            unlocked = level+1;
            //note: we could do something special on unlocked==4
            [g_ResManager storeUserData:[NSNumber numberWithInt:unlocked]
                        toFile:ProgressionSavefile];
        }
        endgame_state = ENDGAME_WINNING;
        [g_ResManager stopMusic];
        [g_ResManager playMusic:@"march.mp3"];
    }
}

We grab the value from our ResourceManager with getUserData: and convert it to an integer. Then we determine whether the current level has not yet been passed; if so, we increment the value and save it back into persistent storage. Regardless, we set our endgame_state to ENDGAME_WINNING and play a triumphant winning march.

If the level completes with a fail, we simply set the endgame_state to ENDGAME_LOSING:

- (void) onFail{
    if(endgame_state == ENDGAME_GAME_IN_PROGRESS){
        endgame_state = ENDGAME_LOSING;
    }
}

In our render function, we perform a spiffy zoom and rotation effect on either the “level complete” or “level failed” image. Once a time period has passed, we also display a “tap to continue” message to cue the player that tapping the screen will now return her to the Main menu. We expect the various GameStates to call this function on their own, at the end of their own render functions, for this to be overlaid in front of their contents:

- (void) renderEndgame{
    if(endgame_state == WINNING) {
        [[g_ResManager getTexture:@"levelcomplete.png"]
        drawAtPoint:
           CGPointMake(self.frame.size.width/2, self.frame.size.height/2 - 64)
        withRotation:min(endgame_complete_time, 0.25f)*4*2*180
        withScale:min(endgame_complete_time, 0.25f)*4];
    } else if (endgame_state == LOSING) {
        [[g_ResManager getTexture:@"levelfailed.png"]
        drawAtPoint:
           CGPointMake(self.frame.size.width/2, self.frame.size.height/2 - 64)
        withRotation:min(endgame_complete_time, 0.25f)*4*2*180
        withScale:min(endgame_complete_time, 0.25f)*4];
    }
    if(endgame_complete_time > 2.0f){
        [[g_ResManager getTexture:@"taptocontinue.png"]
        drawAtPoint:
          CGPointMake(self.frame.size.width/2, self.frame.size.height/2 - 128)
        ];
    }
}

The update and touch functions will count the amount of time that has passed since we entered an end condition and allow the player to change state only if that time has passed:

- (void) updateEndgame:(float) time{
    if(endgame_state != ENDGAME_GAME_IN_PROGRESS){
        endgame_complete_time += time;
    }
}

- (void) touchEndgame{
    if(endgame_complete_time > ENDGAME_HOLD_TIME){
        [m_pManager doStateChange:[gsMainMenu class]];
    }
}

This assumes that all GameState levels want to return to gsMainMenu when they are done.

Now we modify our levels to use these new functions. It will be the same process for each level, so we will review only one level.

In LionLevel.m, we change the Update: function to call onFail if we lost or onWin: if we have won the level:

- (void) Update {

//... code snipped

    for(int i=0;i<lions_length;i++){
        if([m_lions[i] wakeAgainst:m_tom]){
            [self onFail];  //NEW PROGRESSION CODE
        }
        [m_lions[i] update:time];
    }

//... code snipped

    if(m_goal == nil && m_tom.position.y < 8*TILE_SIZE){
        //clear to the door, so win.
        [self onWin:1]; //NEW PROGRESSION CODE
    }

//... code snipped

    [super updateEndgame:time]; //NEW PROGRESSION CODE
}

In the Render function, we simply add a call to renderEndgame just above swapBuffers:

- (void) Render {
    //clear anything left over from the last frame, and set background color.
    glClearColor(0xff/256.0f, 0x66/256.0f, 0x00/256.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    [tileWorld draw];

    [super renderEndgame]; //NEW PROGRESSION CODE

    //you get a nice boring white screen if you forget to swap buffers.
    self swapBuffers];
}

And finally, in touchesEnded:, we call touchEndgame at the bottom of the function:

-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
    UITouch* touch = [touches anyObject];
    [m_tom moveToPosition:
            [tileWorld worldPosition:[self touchPosition:touch]]];
    [super touchEndgame];
}

Modify UI

Now that we have a variable to represent progression, we need to enforce it. We already have an Interface Builder-based GameState with buttons that link to each level. All we need to do is check which ones should be locked and disable them in the initialization of that state.

Inside gsMainMenu.m initWithFrame:, we add the following:

-(gsMainMenu*) initWithFrame:(CGRect)frame
               andManager:(GameStateManager*)pManager
{
    if (self = [super initWithFrame:frame andManager:pManager]) {
        [[NSBundle mainBundle] loadNibNamed:@"MainMenu"
                               owner:self
                               options:nil];
        [self addSubview:subview];
    }

    //NEW PROGRESSION CODE
    UIButton* lvls[] = {
        lvl1, lvl2, lvl3, lvl4
    };

    int unlocked = [[g_ResManager getUserData:ProgressionSavefile] intValue];
    for(int i=0;i<4;i++){
        lvls[i].enabled = i <= unlocked;
}

    return self;
}

It’s as simple as that!

Conclusion

Congratulations, you have created your first iPhone game! You could make many improvements to this game, including adding new levels and modifying the game play of the current levels. But don’t get too distracted because next up is a 3D game!



[1] Film director Alfred Hitchcock was famous for using the term.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required