How to save your game in Cocoa / iPhone

  • strict warning: Non-static method view::load() should not be called statically in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/views.module on line 843.
  • strict warning: Declaration of views_plugin_display::options_validate() should be compatible with views_plugin::options_validate(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display.inc on line 1877.
  • strict warning: Declaration of views_plugin_display_page::options_submit() should be compatible with views_plugin_display::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display_page.inc on line 481.
  • strict warning: Declaration of views_plugin_display_block::options_submit() should be compatible with views_plugin_display::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_display_block.inc on line 193.
  • strict warning: Declaration of views_handler_field_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_field.inc on line 641.
  • strict warning: Declaration of views_handler_argument::init() should be compatible with views_handler::init(&$view, $options) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_argument.inc on line 745.
  • strict warning: Declaration of views_handler_argument_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_argument.inc on line 770.
  • strict warning: Declaration of views_handler_sort_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_sort.inc on line 82.
  • strict warning: Declaration of views_handler_filter::options_validate() should be compatible with views_handler::options_validate($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 585.
  • strict warning: Declaration of views_handler_filter::options_submit() should be compatible with views_handler::options_submit($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 585.
  • strict warning: Declaration of views_handler_filter_broken::ui_name() should be compatible with views_handler::ui_name($short = false) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter.inc on line 609.
  • strict warning: Declaration of views_handler_filter_boolean_operator::value_validate() should be compatible with views_handler_filter::value_validate($form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/handlers/views_handler_filter_boolean_operator.inc on line 128.
  • strict warning: Declaration of views_plugin_row::options_validate() should be compatible with views_plugin::options_validate(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_row.inc on line 135.
  • strict warning: Declaration of views_plugin_row::options_submit() should be compatible with views_plugin::options_submit(&$form, &$form_state) in /usr/www/users/smasher/deadpanic_drupal/sites/all/modules/views/plugins/views_plugin_row.inc on line 135.

Warning to some, bonus to others: this post is less markety and more code-heavy. It also reveals some info about the architecture of "Dead Panic."

Newcomers to iPhoneDevSDK often ask how to convert a string to bytes for writing to a file and other questions about data persistence that belie their real question: how do I save an object and restore it at a later time? NSUserDefaults is fine for storing some strings or bools, but what about an entire custom object? This is called "archiving" and the Apple docs are here.

In short, as long as an object conforms to the NSCoding protocol you can save it to a file  just by calling archiveRootObject:toFile:. Most built-in objects already conform to the protocol; you can save an entire NSArray of  NSStrings, NSNumbers, and NSDictiories with that one line of code. If you want to save your own custom objects, though, you must make them conform to the NSCoder protocol by adding two methods - encodeWithCoder and initWithCoder.

How to save an object

In "Dead Panic" I instantly save the game whenever they press the home button, and restore their saved game whenever the game is launched again. To do that I need to save some information about which map they're on, the player's progress, and the player and enemy positions and health.

Here is my code for saving the map to a coder:

//Map.m
//encode the map data
- (void) encodeWithCoder: (NSCoder *)coder
{

//code the level name
[coder encodeObject: currentLevelName forKey:@"currentLevelName" ];

// code the length of the event list (progress through the level)
NSNumber *listCount = [NSNumber numberWithInt: [eventList count]];
[coder encodeObject: listCount forKey:@"eventList.count" ];

// code the list of players
[coder encodeObject: charList forKey:@"charList" ];

}

Notice I didn't save anything about the structure of the map or the what it looks like - that's all static data I can get elsewhere. When I load this object I'll know the level name, and I can get all of the static data from there.

What about the positions of all of the players? Well, you can see that I'm encoding "charList" above, which is an NSArray of the players. That means encodeWithCoder will be called for each item in that array, so I leave it up to the player class to save the important data for each player. Here's the code for saving a player:

//Player.m
//encode a player or monster character
- (void) encodeWithCoder: (NSCoder *)coder
{
    //save type number
    [coder encodeObject: [NSNumber numberWithInt:type] forKey:@"type" ];

    //save x and y position
    [coder encodeFloat:position.x forKey:@"position.x" ];
    [coder encodeFloat:position.y forKey:@"position.y" ];

    //save health
    [coder encodeInt: health forKey:@"health" ];

}

You can see that there are many things I save, but there's more data that I don't save. I don't save the maximum health of a character, or weapon range, or firing rate, or information about how he is drawn - that info is already in the default constructor, and there is no need to save it redundantly. Avoiding redundancy helps speed up the save process, and it should reduce bugs - a player loaded from a coder should behave the same as one created from scratch.

How to load an object

Of course we need to load the objects too; here is the code for that

//Map.m
//init a map from a coder
- (id) initWithCoder: (NSCoder *) coder
{
    [self init];
   
// load the level name
    self.currentLevelName = [coder decodeObjectForKey:@"currentLevelName"];
   
// load the current event number
    int eventsRemaining = [coder decodeIntForKey:@"eventList.count" ];
   
    // load level based on level name
    [self loadLevel: currentLevelName];

    //skip events until we get to the current event number
    int length = [eventList count];     
    NSRange deletionRange = NSMakeRange(0, length-eventsRemaining);
    [eventList removeObjectsInRange:deletionRange];
   
    //get the list of characters
    NSArray *tempCharList = [coder decodeObjectForKey:@"charList"];
   
    //add chars to map, set team counts
    for (id newChar in tempCharList)
        [self addChar: newChar];
       
    return self;
}

There are some tricks to look at there. I call the default init for the class, then I load a level based on the current level name. This is the same loadLevel method that I use when starting a normal game - I try to keep the custom code for loaded games to a minimum. After I load the level I remove items from the event stack until it matches the length of the saved stack. I could have saved the event stack instead, but again, I'm trying not to duplicate any data.

I also restore the list of characters, but then I add them one at a time using another method of this object - that's because I have some custom code that must be run for every object on the map. Rather than duplicate that code, I call the method the same way it would be called in regular gameplay.

//Player.m
//init a player or monster from a coder
- (id) initWithCoder: (NSCoder *) coder
{   
    type = [coder decodeIntForKey:@"type" ];    //load type number
   
    //init based on type number
    [self initWithType: type];

    // load x and y position
    position.x = [coder decodeFloatForKey:@"position.x" ];
    position.y = [coder decodeFloatForKey:@"position.y" ];
   
    health = [coder decodeIntForKey:@"health" ];

    return self;
}

The only trick here is that I find out what type of player I'm dealing with first, and then init the correct type. Other than that both snippets follow the same pattern - init the object with the regular initializer (the same one I would use when starting a new game) and then apply the information from the saved game.

The final part you can't see is the game information that I discard entirely - like any particles or damage decals on the screen, or currently playing sound effects. These are all short-term effects that don't affect the gameplay, and the player won't miss them when the game reloads. Any ongoing effects - like flames that can damage characters or poisonous clouds to be avoided- would have to be saved, though. You have to decide what is just a visual effect, and what is part of the simulation.

Starting the party

In case you've forgotten how all these methods get called, we kick it all off with these lines:

//load the map from disk; its array of players gets loaded too.
//This causes "intiWithCoder" to be called
myMap = [NSKeyedUnarchiver unarchiveObjectWithFile: filePath];

//take some action if myMap == nil (will happen if save file does not exist)

//save the map to disk; it contains an array of players, so they get saved too.
//This caused "encodeWithCoder" to be called
[NSKeyedArchiver archiveRootObject: myMap toFile:filePath];

That's all there is to it - make your custom objects conform to NSCoder by adding the two methods, try to save only the data that is necessary, and try to use existing initializers and methods wherever possible. Then kick it all off with unarchiveObjectWithFile or archiveRootObject.

PS - be sure to add <NSCoding> to the list of protocols for your object in the .h file to eliminate any warnings.