Game State Serialization

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.

Initialize Storage

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"

End Game

Within our GLESGameStates, 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 GameStates 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];
}

Modify UI

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

It’s as simple as that!

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.