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.”
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 encodeObject: [NSNumber numberWithFloat:position.x] forKey:@"position.x" ]; [coder encodeObject: [NSNumber numberWithFloat:position.y] forKey:@"position.y" ]; //save health [coder encodeObject: [NSNumber numberWithInt: 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 decodeObjectForKey:@"eventList.count"] intValue]; // 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 decodeObjectForKey:@"type" ] intValue]; //load type number //init based on type number [self initWithType: type]; // load x and y position position.x = [[coder decodeObjectForKey:@"position.x" ] floatValue]; position.y = [[coder decodeObjectForKey:@"position.y" ] floatValue]; health = [[coder decodeObjectForKey:@"health" ] intValue]; 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.
*This tutorial written by Smasher
Thanks Smasher!
How to save your game (or any object) on the iPhone