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

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

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