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.
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 GLESGame
State
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.
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.
Again you can see the unique tile strip that will be loaded (see Figure 4-7).
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
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.
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]; }
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).
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; }
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]; }
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; }
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.