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
}

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.