Advertise Mobile SDKs Books Events Forum News Social Networking Support Us
Follow @iphonedevsdk on Twitter

Interface 2, Advanced iOS
Mockup & Code Gen
($9.99)

Make your own iPhone apps
and run them live!
(free)

Pic Frame Dynamo: Photo Editing
($0.99)

Abiliator
($1.99)

Want your application or service advertised on iPhone Dev SDK?

Go Back   iPhone Dev SDK Forum > iPhone SDK Development Forums > iPhone SDK Tutorials

Reply
 
LinkBack Thread Tools Display Modes
Old 12-01-2009, 05:41 PM   #1 (permalink)
Shmoopi LLC
 
Shmoopi's Avatar
 
Join Date: Jun 2009
Location: Virginia
Posts: 213
Shmoopi is on a distinguished road
Lightbulb How to save your game (or any other object) in Cocoa

Smasher,

I've written a tutorial on how to save custom objects to disk, and I wanted to post it here before putting it on my site. I'd love any feedback or questions.

----

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:

Code:
//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:
Code:
//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
Code:
//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.
Code:
//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:
Code:
//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!
Shmoopi is offline   Reply With Quote
Old 12-10-2009, 05:53 PM   #2 (permalink)
Registered Member
iPhone Dev SDK Supporter
 
smasher's Avatar
 
Join Date: Jul 2008
Location: San Mateo, CA (San Fran)
Posts: 3,858
smasher will become famous soon enough
Default

Thanks for reposting this Shmoopi. I've also put it in its permanent home on deadpanic.com :

How to save your game (or any object) on the iPhone

I still welcome any feedback from people who tried or liked this tutorial.
__________________

Free Games!
smasher is offline   Reply With Quote
Old 07-10-2010, 01:09 AM   #3 (permalink)
Registered Member
iPhone Dev SDK Supporter
 
smasher's Avatar
 
Join Date: Jul 2008
Location: San Mateo, CA (San Fran)
Posts: 3,858
smasher will become famous soon enough
Default

I realized -- months later, looking at this code -- that I had some kind of brain fart when I wrote it. NSCoder has methods for directly encoding floats and ints, so I didn't need to encapsulate everything in NSNumber the way I did. In particular, code like this:

Code:
    [coder encodeObject:
        [NSNumber numberWithFloat:position.x] forKey:@"position.x" ];
would be better written like this:

Code:
    [coder encodeFloat:position.x forKey:@"position.x" ];
I already updated the version on my site.
__________________

Free Games!
smasher is offline   Reply With Quote
Old 11-11-2010, 06:58 PM   #4 (permalink)
Registered Member
 
Gomfucius's Avatar
 
Join Date: May 2009
Location: San Francisco
Posts: 47
Gomfucius is on a distinguished road
Default

Hello,
I'm using the same way to save game data. My question is, how secure is this? Can someone take the saved data file and alter it? I don't want this to happen because I'm posting highscore to Game Center from saved data. If people are able to alter the saved data, I would have to use keychain, but I'd rather not introduce new tech to save my game data.
Thanks!
Gomfucius is offline   Reply With Quote
Old 11-13-2010, 10:43 AM   #5 (permalink)
Registered Member
iPhone Dev SDK Supporter
 
smasher's Avatar
 
Join Date: Jul 2008
Location: San Mateo, CA (San Fran)
Posts: 3,858
smasher will become famous soon enough
Default

It would not consider it too secure. Someone with a jailbroken phone could copy the file in or out of the documents dir. It's harder to read than a text file (I think the binary format is the default) but it's not encrypted.

You could make it more secure by adding a checksum of some game data plus a salt - on load you can check if the checksum still matches the data loaded from a file. They could still read the file at that point, but not modify it. To hack that they'd have to guess/decompile your salt and checksum method or modify your binary to short-circuit the check.
__________________

Free Games!
smasher is offline   Reply With Quote
Old 11-19-2010, 04:32 PM   #6 (permalink)
Registered Member
 
Gomfucius's Avatar
 
Join Date: May 2009
Location: San Francisco
Posts: 47
Gomfucius is on a distinguished road
Default

ah ok, thanks Smasher!!! checksum is what I use to upload data to my server. I guess I can use for local files too!
__________________
Dungeons & Such - Nostalgic Dungeon RPG http://bit.ly/9CfnfF
Gomfucius is offline   Reply With Quote
Reply

Bookmarks

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is On
Trackbacks are On
Pingbacks are On
Refbacks are On



» Advertisements
» Online Users: 463
15 members and 448 guests
alexeir, David-T, Dj_kades, foslock, HemiMG, iAppDeveloper, jeroenkeij, LunarMoon, Mijator, Pauluz85, pipposanta, QuantumDoja, robsmy, sacha1996, usernametaken
Most users ever online was 1,387, 04-10-2012 at 04:21 AM.
» Stats
Members: 175,679
Threads: 94,129
Posts: 402,928
Top Poster: BrianSlick (7,990)
Welcome to our newest member, xzoonxoom
Powered by vBadvanced CMPS v3.1.0

All times are GMT -5. The time now is 09:23 AM.
Powered by vBulletin® Version 3.8.0
Copyright ©2000 - 2012, Jelsoft Enterprises Ltd.
Search Engine Friendly URLs by vBSEO 3.3.0