How to Save your Game’s Data: Part 1/2

Version

Other, Other, Other

Learn how to save your game’s data!

Unbelievably, in the “old days” many games lacked the ability to save data. You would literally have to beat the game in one sitting! This often caused players to experience emotions ranging from passing frustration to explosive rage, as Angry Video Game Nerd can attest.

In the great days of modern iOS, your game has no excuse because there are plenty of ways to save data, from NSUserDefaults to Core Data to iCloud and more!

In this two-part tutorial, you’re going to learn the best ways to save data in your games. Specifically, you’ll learn how to:

Separate your data: You’ll build a class to hold your game data and update it properly during gameplay.

Save your data: You’ll use NSCoding to persist your game data to the device.

Use iCloud: You’ll also learn how to use iCloud to allow the user to continue their game on a different device if they so desire.

By the end of both parts, you’ll have an awesome iCloud connected Space Shooter made with Sprite Kit:

And yes, that’s a cat in the cockpit. Surely you know cats are avid pilots?

Get ready to start saving!

Getting Started

For this tutorial, you’ll build upon the game created in Tony Dahbura‘s Sprite Kit Space Shooter Tutorial. These exercises will be most useful if you go through that tutorial first, but if you’d rather not, at least scan the code so you’re familiar with how it works.

Open up Space Shooter in Xcode, then build and run on an iOS device. You’ll need to run the game on an actual device since the accelerometer is required to steer the spaceship around all those asteroids.

To win the game just stay alive for 30 seconds. All you have to do is blast the asteroids out of your way to avoid deadly collisions. It’s not as easy as it sounds!

Game Data Class

Did you notice that the game doesn’t track the score, or the distance traveled by the ship? You’ll start by adding tracking for those two metrics so that you have something interesting to save across gameplay sessions.

In this section, you’ll create a new class to hold the game data. It’s a best practice to isolate your game’s data from the rest of the game; this abstraction makes it much easier to save the user’s progress from game to game.

In Xcode, go to File\New\File…, choose the iOS\Cocoa Touch\Objective-C class template and click Next. Name your new class RWGameData, make it a subclass of NSObject, click Next and then Create.

RWGameData will hold your game’s data, specifically, the score and distance traveled for the current game, as well as the player’s highest score and total distance flown. Open RWGameData.h and replace the default contents of this file with the following code:

This is a boilerplate Objective-C implementation of the singleton pattern. Using sharedGameData throughout your app will ensure that you’re always accessing the same RWGameData instance. You can use this same code any time you want to provide singleton access to one of your own classes.

Next, take care of the reset method, which will reset the game’s score and distance whenever the user starts a new game. Add this method inside RWGameData.m:

-(void)reset
{
self.score = 0;
self.distance = 0;
}

In this method you just set the score and distance properties to 0, so the user can start fresh.

Pimping up the HUD

Now that you have a class to store the game’s data, now you’ll modify the game to keep score with the new class.

Open MyScene.m, where the game logic resides, and add the following line at the top of the file to import your new class:

#import "RWGameData.h"

You’ll need a few labels to display progress to the user. Add the following instance variables to the @implementation section in MyScene.m:

SKLabelNode* _score;
SKLabelNode* _highScore;
SKLabelNode* _distance;

Still in MyScene.m, add the following method to initialize those labels and add them to the scene:

This will set the high score label to whatever you stored last in RWGameData‘s highScore property and will reset the values of _score and _distance. You’re all ready to go! Build and run the project right now.

The HUD is now visible, but the game doesn’t keep score yet – that’s the next thing to add to the app.

Keeping Score

There’s no sense in playing the game if you’re not scoring points, right? Every time the player shoots an asteroid he or she should earn a point and the score should update.

Stay in MyScene.m, scroll to update: and look for the if statement that checks for [shipLaser intersectsNode:asteroid]. This code branch executes whenever a ship laser contacts an asteroid – the code hides the laser and the asteroid, then spawns a new explosion on the position of the impact. Boom!

This is a good spot to add the score-keeping code. Add the following just above the continue; statement at the end of this if block:

First, you increase the player’s score by 10 points, then you update the label’s text with the new amount. Build and run, then shoot a few asteroids to try out the scoring system:

If you play a couple of games you’ll notice the score does not reset between games. But why does this happen? You certainly remember writing a reset method in RWGameData, right?

Long story short, you need to add code to invoke reset. Doh!

You want to reset the game data both when the user fails and completes the game. In MyScene.m, just above the line that calls removeAllActions:, add the following line of code inside endTheScene:.

[[RWGameData sharedGameData] reset];

This line will take care of resetting the per-game data every time the game is over. Build and run to see this in action. The score will now reset every time you restart the game.

Next, you’re going to update the high score at the end of every game – but you’ll need to check if the current score is higher than the stored highest score, and update the high score as needed. To make the reward more complex, you’ll need to implement a more complex reward logic that updates the highest score only when the player survives for at least 30 seconds.

The code that detects a successful completion of the game is towards the end of update: in MyScene.m. Find this line [self endTheScene:kEndReasonWin]; and add the following code above it to update the high score:

The MAX() function compares the current score to the high score and sends back the bigger number. You just set [RWGameData sharedGameData].highScore to that number and that’s it. Adding it is easy enough when you know where to put that line in the project’s code.

Next, you’re going to track the distance the space ship travels during a game.

Still inside update: in MyScene.m, add the following code a bit above where you just updated highScore and immediately before the if statement that checks whether _lives <= 0:

update: and currentTime parameters hold the time the method was called. The above code compares currentTime against _lastCurrentTime to see if at least one second passed since it last updated the distance.

To update the distance, you do a few things:

You increase the distance traveled this game and the total distance traversed across all games.

You update the distance label's text. Note you're displaying the total distance across all games, so it's easy to see if this saves properly when you add that later on.

Finally you update _lastCurrentTime, which ensures you won't update distance again until another second has passed.

Persisting Data Between Launches

You're doing well so far, but did you notice that every time you launch the game, the high score and the total distance resets to zero? That's not really the result you're after, but it's helpful to take baby steps as you learn how to persist data between app launches.

Your next step will be to make the RWGameData class conform to the NSCoding protocol, which is one good way to persist game data on a device. The advantage of NSCoding over alternative persistence methods like Core Data is that it's nice and easy, and is ideal for a small amount of data like what you see with this game.

The NSCoding protocol declares two required methods:

encodeWithCoder:: This method converts your object into a buffer of data. You can think of it as "serializing" your class.

initWithCoder:: This method converts a buffer of data into your object. You can think of it as "deserializing" your class.

It's quite simple really - you have to implement one method for saving and one for loading; that's all there is to it. Now, you're going to see how precise data storage can be.

Open RWGameData.h and modify the @interface line so it looks like this:

@interface RWGameData : NSObject <NSCoding>

This declares that RWGameData conforms to the NSCoding protocol.

Switch back to RWGameData.m. You'll add encodeWithCoder: (just think of it as the method to "save" data) and two constants for the key names you'll use to store the data when encoding the class. Add the following code just below the @implementation line:

encodeWithCoder: receives an NSCoder instance as a parameter. It's up to you to use this to store all the values you need persisted. Note that you'll persist only the high score and total distance. Since the other properties reset between games, there's no need to save them.

You probably already figured out how the encoding works. You call a method called encodeXxx:forKey: and provide a value and a key name, based on the type of your data. There are methods for encoding all the primitive types, like doubles, integers or booleans.

There's also a method to encode any object that supports NSCoding (encodeObject:). Many of the built-in classes like NSString, NSArray or NSDictionary implement NSCoding. You can always implement NSCoding on your own objects, much like how you're doing it here.

Note: If your class extends anything that conforms to the NSCoding protocol, you must call [super encodeWithCoder:encoder]; to ensure all of your object's data persists.

This is everything you need to do in this method, just supply values and keys to the encoder. Actually saving to the device is a separate task on your list, which you'll take on in a moment.

Now it's time to implement the opposite process - initializing a new instance with the data from a decoder. Add the following method to RWGameData.m:

See how you start this method much as you would any other initializer, by calling some initializer on your parent class? If your class extends anything that conforms to the NSCoding protocol, you most likely need to call [super initWithCoder:decoder], but because this class extends NSObject, calling init here is fine.

Much the same way you used encodeDouble:forKey: to store a double value for a given key, you now use decodeDoubleForKey: to retrieve a double value from the NSCoder instance passed into the method.

By implementing these two methods, you added the ability to your class to save its current state and retrieve it with ease.

I'm sure you're eager to take your new NSCoding class for a test drive. However, you'll need to hold your horses just a bit longer. You still need to save the game data to a file.

First, you need to make sure the class will create a new empty instance whenever there's no persisted data, i.e. the very first time you run the game.

Inside RWGameData.m, add this simple helper method to construct the file path to store the game data:

First you get the file path where the stored game data file should be and then you try to create an NSData instance out of the file contents.

If decodedData is not nil, then that means the file content was read successfully and converted to an NSData instance. In that case (pay attention…here comes the magic) you create an RWGameData instance by calling NSKeyedUnarchiver's class method unarchiveObjectWithData:.

What unarchiveObjectWithData: does is to try to initialize a new RWGameData by invoking its initWithCoder: initializer with an NSCoder loaded with decodedData. (Try saying that aloud, three times, fast; it's a mouthful!)

In case decodedData was nil you just construct a new instance of the class by calling init.

One final touch for RWGameData.m - in sharedGameData, replace this line:

sharedInstance = [[self alloc] init];

with this one:

sharedInstance = [self loadInstance];

This will ensure that when you create the instance of the game data class it loads the contents of the previously stored file, provided one exists at the target file path.

I don't always write a load method, but when I do I write a save method too.

That's a good piece of advice just there. Add a save method to RWGameData.m, as well:

This code is the exact reverse of what you implemented in loadInstance. First, you call [NSKeyedArchiver archivedDataWithRootObject:] to get encoded data out of your class instance; this calls encodeWithCoder: on your instance behind the scenes. Then writing the data to file is simply a matter of calling writeToFile:atomically:.

Remember to switch to RWGameData.h, and also to add the method signature inside the class interface:

-(void)save;

And that's all there is to archiving and unarchiving data using the device. Now you only need to call save every now and then, and you're good to go.

Open MyScene.m and add the following line to endTheScene:, just before the line that calls reset on the RWGameData singleton:

[[RWGameData sharedGameData] save];

This wraps up this section, so now it's time to give your game a try! Build and run, then play a few to see your high score persist between launches:

Then stop the game from Xcode and launch the project again. When you start the game you'll see the high score you achieved during the previous launch. Success!

More about NSCoding

Are you mumbling something under your breath? Did you say something to the effect of, "Yeah, when you need to save two double numbers it's all too easy"? Well, you're on the right track. There is more you can do with NSCoding, but the concept is remarkably similar to what you just did.

As I mentioned earlier, NSCoding includes helper methods to encode various types of primitives:

encodeBool:forKey:

encodeInt:forKey:

encodeInt32:forKey:

encodeInt64:forKey:

encodeFloat:forKey:

You can encode any object that implements NSCoding with this:

encodeObject:forKey:

If you can convert something to either NSData or a series of bytes, you can encode it with these methods:

encodeBytes:length:forKey:

encodeDataObject:forKey:

And finally, there are a few utility methods to ease the pain whenever you need to encode certain common structures:

Say "Space Cheese"!

Are you ready to try something more interesting than storing numbers? Good! Next, you'll add the player's photo to RWGameData and save it to the device.

To work with images you'll need some extra assets unrelated to this tutorial, so download them now.

Unzip it and drag the ImageMasking folder into your Xcode project. Be sure you select both of the following: Copy items into destination group's folder (if needed) and Create groups for any added folders. You should see three new files in your project navigator:

The files are as follows:

25_mask.png: This is a black and white mask matching the shape of the front window of the spaceship.

UIImage+Mask.h: This is a category on UIImage that provides a method to resize and mask a UIImage instance.

UIImage+Mask.m: This is the implementation of the method mentioned above.

What you're about to do is let the player take a photo, scale it, mask it and display it in the space ship's window.

Now, add the code for taking a photo. Open MyScene.m and in the file contents, close to the @implementation line, add a new instance variable under the rest of the ones you added earlier:

SKLabelNode* _takePhoto;

This instance variable will hold your new HUD button for making photos. Now scroll down to setupHUD: and add this code at the end of the method body:

This will create a new label along the rest of the HUD, but this label will act like a button - just like the label that acts like a button to restart the game. You also need to add the code to handle touches.

Scroll to touchesBegan:withEvent:, and add this new chunk of code to handle touches on the Pilot photo button. Check the comments inside the code below to make sure you're adding the right code:

What this new code does is to check whether the name of the node being tapped equals to "TakePhotoButton", and if so invokes the takePhoto: of your scene, which you'll add to the class implementation now:

This should be enough to see the camera view controller, so give it a try! Build and run, then tap the Pilot photo button on the top of the scene.

Note: Unfortunately, you can only run this portion of the code on a device. If you try to run it in the simulator, it will crash with a NSInvalidArgumentException since the camera is not available.

Oooops! You should see an error in the console like this:

Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation',
reason: 'Supported orientations has no common orientation with the application,
and shouldAutorotate is returning YES'

This was (almost) unexpected. So, to find the trouble-maker, click on the project file and check the allowed screen orientations:

Bingo! The image picker view controller only supports portrait orientation, but the app doesn't support portrait - so this seems easy enough to correct, right? Just check the portrait checkbox and build and run.

Hmm…now when you tilt the device in a certain position the whole scene rotates and the display looks off-kilter. As it turns out, your game can't support both portrait and landscape orientations.

Now you're going to implement a little trickery to support only landscape while the player plays the game, and only portrait while they are taking a photo.

Open ViewController.h and add a new static variable above the @interface line:

static BOOL lockToPortraitOrientation = NO;

This is a static variable, which you'll set to YES when you present the image picker and set back to NO when the player wants to continue playing.

Switch to MyScene.m and add along the rest of the imports at the top of the file:

#import "ViewController.h"

Add the following as the first line of code in the takePhoto:

lockToPortraitOrientation = YES;

Most of your master plan is now completed, the only thing remaining is to make ViewController respect the value of lockToPortraitOrientation. Open ViewController.m and find the method called supportedInterfaceOrientations. Replace the contents of the method with the following:

The code is straight forward; if lockToPortraitOrientation is YES the view controller allows only portrait orientation; otherwise it allows landscape only. Could something so simple actually work? Give it a try!

The camera view shows up and you can snap a picture, nice! But did you notice there's another problem? update: doesn't pause while you take a photo. There's a quick fix for that too, just open MyScene.m and as first line of code in update: add:

if (self.paused) {
return;
}

This stops all node actions while you're taking your mug shot. That's more like it!

Immortalizing Your Photo

Now you need to make sure the photo is useful to the player. In this section, you'll add the delegate methods to the image picker view controller to make sure your handsome face saves to the disc, along your high score and total distance flown.

First open RWGameData.h and add a new property:

@property (strong, nonatomic) UIImage* pilotPhoto;

Switch to RWGameData.m and along with the other constants near the top of the file, add this:

static NSString* const SSGameDataPilotPhotoKey = @"pilotPhoto";

This will be the key you use to send the photo to the encoder/decoder.[TODO:I replaced 'encode' with 'send' to reduce redundancy. Please confirm the word choice is suitable] Speaking of which, scroll to encodeWithCoder: and add the following to the end to encode the photo property:

First, you check whether there's a pilot's photo. You're going to call this method when the game starts, so you need to check.

Then you make sure the photo is properly sized and masked, because once you persist it via the encoder the masking will actually disappear.

Check for an existing pilot photo node with name of "Pilot" and if found, remove it from the scene.

Create a new texture from the pilot photo and create a sprite node out of the resulting texture. Finally, position the sprite node and add it as a child to the space ship.

Your app calls this method when you capture a new photo, and also when you build the scene and load the game data from a file. If the player sets his or her photo in a previous app launch, you'll need to load the photo when the game starts.

Do that in initWithSize:. Just find the line where you add the ship to the scene: [self addChild:_ship]; and add this above:

[self setupPilot];

Perfect! Now when you start the game the app will check for persisted photo, and if found, it'll display on-screen. If the player takes a new photo - it overwrites and saves the new one to the game data file.

Build and run. You should be able to take your photo and see yourself piloting that space ship! Yeah!