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.
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.
The tile world should look like Figure 4-11.
Figure 4-12 shows the unique 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.
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; } } }
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"; } }
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.
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).
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.
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.
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]]; }
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.