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.
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"
Within our GLESGameState
s, 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 GameState
s 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];
}
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; }
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.