Welcome to part two of this tutorial series that’s all about learning how to save data in a game!
In the first part of this series, you learned how to store your game’s data in a separate class, and save it to disk using NSCoding
.
In this second and final part of the series, you’ll:
- Take on cheaters and deal with them once and forever
- Add iCloud capabilities to your game.
Feel free to continue coding from where you left off, or you can download the project, completed to this point.
Now enable your hacking scanners and set your ship’s auto-pilot to iCloud City!
Getting Started
So far everything is perfect in your game-data-land; you track the player achievements, store them to the disc, and you even have a pilot photo showing somewhere there!
Now, imagine this scenario coming to life before your very eyes: Peter is a top-notch Space Shooter, and he achieves a high score of 10,000 points. He then loads his game on Stan’s phone, which overwrites Stan’s game data. Since the file has no protection so now Stan’s high score is also at 10,000 points! Peter is upset because now Stan is doing a victory dance and tweeting out that he’s the king of Space Shooter.
With a simple game like Space Shooter, this problem isn’t a big deal. Well, except for Peter, but he’ll muddle through somehow. However, you can probably imagine a case where this would be a big problem, for example, if you were to send the high score to a leaderboard server.
The first thing that comes to mind when you need to add a smidgen of security to an app is to store the data in the user Keychain. However, it’s designed to store short pieces of text, like passwords, and you need to store more complicated data like photos. Luckily, there is a workaround for this. By the time you’re at the end of this tutorial, you’ll be ready to start working with it in your projects.
When you use Keychain to save the game data to the disc, you’ll also store a hash of the file contents in the Keychain. What does that mean? A malicious user could possibly read the data, and for example see the high score, but they won’t be able to tinker with anything without invalidating the hash.
It won’t take too long to implement the necessary modifications. First, download the KeychainWrapper class, which will make it easier for you to work with the device Keychain storage:
Note: The KeychainWrapper
comes to you from another useful tutorial on RayWenderlich.com; check it out to learn more about the Keychain: Basic Security in iOS5, by Chris Lowe. Please note there are a couple of very small modifications to the source so that it’s a better fit for this tutorial.
Extract the contents of the zip file and copy the two resulting files to your project. You should see KeychainWrapper
‘s files visible, like so:
Open RWGameData.m. At the top file, import the Keychain helper class header file.
#import "KeychainWrapper.h" |
Underneath the @implementation
direction, add the following:
static NSString* const SSGameDataChecksumKey = @"SSGameDataChecksumKey"; |
Since you’re tracking the hash, you’ll need to update it when you save the game data. Can you guess where it will to go? Scroll down to the save
method.
KeychainWrapper
sports a rather handy method that will help you achieve your goal. Add this line to get the SHA256 hash of encodedData
to show up at the end of save
:
NSString* checksum = [KeychainWrapper computeSHA256DigestForData: encodedData]; |
computeSHA256DigestForData:
gets in an NSData
instance and returns the SHA256 as a string, which is pretty handy to store in the Keychain. Add this code:
if ([KeychainWrapper keychainStringFromMatchingIdentifier: SSGameDataChecksumKey]) { [KeychainWrapper updateKeychainValue:checksum forIdentifier:SSGameDataChecksumKey]; } else { [KeychainWrapper createKeychainValue:checksum forIdentifier:SSGameDataChecksumKey]; } |
You use keychainStringFromMatchingIdentifier:
to check whether a value for the given key exists, and if so, you update it with updateKeychainValue:forIdentifier:
. If not, you create new value storage for the checksum key createKeychainValue:forIdentifier:
.
One part of the operation is now complete. Every time you save the game data file, you also store its checksum securely in the Keychain. Next, you’ll change the code so that it loads a stored file only when the file checksum is identical to most recently saved game. Locate this piece of code in loadInstance
:
if (decodedData) { RWGameData* gameData = [NSKeyedUnarchiver unarchiveObjectWithData:decodedData]; return gameData; } |
Currently the class unarchives provided data into an instance and returns it straight away. Replace the whole chunk of code above with this new logic:
if (decodedData) { //1 NSString* checksumOfSavedFile = [KeychainWrapper computeSHA256DigestForData: decodedData]; //2 NSString* checksumInKeychain = [KeychainWrapper keychainStringFromMatchingIdentifier: SSGameDataChecksumKey]; //3 if ([checksumOfSavedFile isEqualToString: checksumInKeychain]) { RWGameData* gameData = [NSKeyedUnarchiver unarchiveObjectWithData:decodedData]; return gameData; } //4 } |
That’s more like it! Here’s what you did when you added this new code:
- You generate the SHA256 hash of the decoded data and store it in
checksumOfSavedFile
- Then you get the most recent SHA256 hash from the Keychain and store it in
checksumInKeychain
. If you get decoded data, you must’ve stored its hash in the Keychain before. - Finally, you compare both checksum strings. If they are equal, you unarchive the data and return the resulting
RWGameData
as the result. - If the checksums don’t match – you don’t do anything. The code execution continues, and the method returns a new blank
RWGameData
on the next line.
Your basic security is in place! Good job!
Note: How you punish the user for tinkering with their game data file is entirely in your hands. In this tutorial, you’ll learn how to take a subtle approach by quietly deleting their high score and distance flown, but hey … you can be mischievous too and do things like show a sassy meme or alert the cheat police — any punishment you feel fits the crime.
The all time winner goes to the makers of Serious Sam 3. When the game detected that it was pirated, it spawned – midgame, no less – a player hunting indestructible pink scorpion that pretty much ended the game.
Build and run the project again. Look at that! Your high score is back to zero, but you didn’t modify your game file. Can you guess what happened? Try to figure it out. If you give up, click below for the solution.
Now, play a round or two to set a new high score, and check whether everything is working. What’s the highest score you can achieve?
Enabling iCloud
Unless you’ve spent the last five years living in a cave on the moon, then you know “the cloud” is all the rage these days. When you have the cloud you can: do some work on your laptop, continue working on your iPhone in the subway and then pick up where you left on your iPad during your flight; it’s all connected as long as your iOS device is connected to iCloud.
With so many iOS devices in your life that are suitable for gaming (c’mon Apple we want a gaming Apple TV already!), you’ll definitely want the ability to pick up any of your devices and continue blasting asteroids to smithereens. You’ll also want to have your pilot photo and high scores synchronized across your iOS devices, right?
Luckily, enabling iCloud for your game is straightforward. Here’s how you set your game up to take advantage of the cloud
Select the SpaceShooter project file in the project navigator and select the SpaceShooter target. Click on the Capabilities tab.
Click the ON/OFF switch to toggle its state to ON.
The settings will change to look like the following image:
Look at the project file list to make sure Xcode completed the listed steps. Look for a new entitlements file that enables iCloud capability for the game:
So far, so good! The next step is to enable key-value storage for your game; you’ll use this kind of iCloud storage to persist the game data between synchronized devices. That’s technology at its best.
Believe it or not, Apple’s iCloud service already backs up your game.
Time to put your head in the cloud. Literally
The next question is, “How do I get my handsome mug shot synced across all my devices?”
The logic behind storing the game data is a little bit more complicated. Consider the following:
- The user might or might not have iCloud enabled on their devices. Therefore, local storage should be primary, with iCloud as an optional secondary storage.
- The user might have played for a while, and only used disc storage. Then one sunny day they might decide to turn iCloud on, and the game will need to handle this situation properly, i.e. not delete the locally stored high score.
- The user could delete the game completely and then re-install it. In this scenario you need to initialize the game data from iCloud the first time it launches.
So there’s a little more to it than just syncing to iCloud. Fortunately, for each of these scenarios there is a solution, and by the end of this tutorial you’ll know what to do to manage the most common.
In the beginning of this tutorial, you read that you’ll keep some game data in the cloud and some will save to the device. This means you’ll:
- Check if the local high score is higher than the iCloud high score, and update the one in iCloud
- Store the total distance flown locally, which effectively keeps track of the distance the player flies the ship on a particular device
- Share the pilot photo in iCloud and monitor for changes across synced devices. This way if you take a new selfie on your iPhone, it will change on your iPad too
Here’s a recap of the final logic for storing the game data:
score |
Stored only in memory; resets when a new game starts. |
distance |
Stored only in memory; resets when a new game starts. |
totalDistance |
Stored on disc; persists between app launches, and stored on single device. |
highScore |
Stored on disc, and always compared to iCloud. The highest number is used. |
pilotPhoto |
Stored on disc; stored in iCloud. The latest photo taken overwrites the one in iCloud. |
So far, you have all of these in working order, except for the high score and pilot photo.
Beginning iCloud in iOS 5 and iCloud and UIDocument: Beyond the Basics. Also, you should check out our book, iOS 5 by Tutorials, which has been recently updated for Xcode 5 and iOS 7.
Updating iCloud
Open RWGameData.m and add this new method anywhere inside the class body:
-(void)updateiCloud { NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; long cloudHighScore = [iCloudStore doubleForKey: SSGameDataHighScoreKey]; } |
Since you’ll need to store just a couple of key-value pairs in your iCloud bucket, you’ll automatically work with the default storage. NSUbiquitousKeyValueStore
is the class that provides you access to iCloud data. You can think of NSUbiquitousKeyValueStore
as an NSDictionary
in the cloud :]
After you get the default NSUbiquitousKeyValueStore
, you use its doubleForKey:
. It’s similar to what you would use with an NSCoder
to get the stored high score value. Use the same constant for name of the key when archiving/unarchiving RWGameData
. In the end, why not? The key name is just a string constant!
Now that you have the high score from iCloud, it’s time to compare it to the local data. Add these few lines:
if (self.highScore > cloudHighScore) { [iCloudStore setDouble:self.highScore forKey: SSGameDataHighScoreKey]; [iCloudStore synchronize]; } |
The code is simple enough. If the local high score is more impressive than the one on iCloud, then it’s time to update iCloud. setDouble:forKey:
sets the value for the high score key and finally a call to synchronize
fires up an iCloud sync.
Note: If you have ever used NSUserDefaults
you probably already feel at home. The iCloud API and device key-value storage are similar by design.
Next, scroll to the method called save
and add at the end:
if([NSUbiquitousKeyValueStore defaultStore]) { [self updateiCloud]; } |
Checking if [NSUbiquitousKeyValueStore defaultStore]
returns a class instance tells you whether iCloud is enabled or not. After the game data saves to the disc you check if iCloud is enabled, and if so, you call your shiny and new updateiCloud
method.
That takes care of updating the high score to iCloud. Next, you’ll sync your photo.
Consider what you’re going to do for a moment. The logic for the high score is simple enough; compare two numbers and if the local one is bigger about the game updates the data on iCloud. Can you do the same for the photo? You certainly can’t compare photo sizes. What you could do is keep the last modified date of the photo separate and compare the last modification date of both photos. That could work, but there’s a better way.
You’re going to be ultra-smart about updating the photo to iCloud because you’ll implement a custom setter for the pilotPhoto
.
Add this new method to RWGameData
:
-(void)setPilotPhoto:(UIImage *)pilotPhoto { //1 _pilotPhoto = pilotPhoto; //2 if([NSUbiquitousKeyValueStore defaultStore]) { //3 NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; //4 NSData* imageData = UIImagePNGRepresentation(pilotPhoto); [iCloudStore setObject:imageData forKey: SSGameDataPilotPhotoKey]; [iCloudStore synchronize]; } } |
Here’s what the code does:
- First, you store the new image in the backing instance variable
_pilotPhoto
. - Then, just as before, you check whether iCloud is enabled by probing the default store.
- You fetch the default store in
iCloudStore
. - Finally, you get the PNG data out of the photo and store it in your key-value iCloud storage. You wrap up with a call to storage’s
synchronize
method.
Now every time the player takes a new photo it will upload to iCloud. Nice.
Fetching Data from iCloud
So far, you have the code in place to update the iCloud key-value storage with your local data. But how about fetching data from iCloud and updating your local storage?
Lucky for you iCloud shouts out whenever there are pending changes. You can simply observe the NSUbiquitousKeyValueStoreDidChangeExternallyNotification
notification and update your local storage accordingly.
Since you want to start listening for iCloud changes as soon as RWGameData
instance is created, the best way to do that is with a custom init
.
Add the following code underneath init
.
- (instancetype)init { self = [super init]; if (self) { //1 if([NSUbiquitousKeyValueStore defaultStore]) { //2 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateFromiCloud:) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil]; } } return self; } |
updateFromiCloud:
yet … so stay calm … that’s coming next :]This is a classic init
with couple of code additions:
- You check whether the device has enabled iCloud.
- You look for a
NSUbiquitousKeyValueStoreDidChangeExternallyNotification
in case iCloud is enabled.
This ensures that every time a change comes in from iCloud, updateFromiCloud:
will fire up. For example, if you have the game open on two devices and take a photo on one of them.
Now you’ll code the observer method. Add it to the class with few initial lines of code:
-(void)updateFromiCloud:(NSNotification*) notificationObject { NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore]; long cloudHighScore = [iCloudStore doubleForKey: SSGameDataHighScoreKey]; self.highScore = MAX(cloudHighScore, self.highScore); } |
You’re probably already super-familiar with this kind of code. You simply fetch the default store and you get the iCloud high score in cloudHighScore
. In the end, you assign the highest score from the local and iCloud data to self.highScore
.
High score is all set, so now onto the pilot’s photo. Add this code to the bottom of updateFromiCloud:
if ([iCloudStore objectForKey: SSGameDataPilotPhotoKey]) { NSData* imageData = [iCloudStore objectForKey: SSGameDataPilotPhotoKey]; if (imageData) { _pilotPhoto = [[UIImage alloc] initWithData:imageData]; } } |
When unarchiving data from the game data file, you check for a stored photo, and if there is one you fetch the persisted NSData
and use it to create a UIImage
. You update RWGameData
and save the image as the current pilot photo.
Do you know why you used the backing instance variable _pilotPhoto
to set the photo instead of setting directly the property self.pilotPhoto
? Try to figure it out. When you’re ready, click below to expand the answer.
Now you’ve done all the necessary updates. To make sure all of the changes are persisted, also add a call to save the game data file to the disc.
[self save]; |
So you’ve stored data to iCloud and fetched with no (hopefully) problems. Don’t run the game just yet though.
Since iCloud is accessible via network, access and changes distribution are not instantaneous. Sometimes you’ll start the game and a few seconds into the gameplay the new photo will show up. Therefore, you need a way to update the game data and the scene changes that come in during gameplay.
You’ve already done half of the work, because you receive a notification when changes come in from iCloud. You just need to fire another notification when you’ve updated the game data so that MyScene
knows to update the UI too.
Open RWGameData.h, and under the import statement define your notification’s name constant:
static NSString* const SSGameDataUpdatedFromiCloud = @"SSGameDataUpdatedFromiCloud"; |
Naturally, you’ll fire the notification just after you update the game data from iCloud. Switch back to RWGameData.m and find updateFromiCloud:
. Just add this line to the end of it:
[[NSNotificationCenter defaultCenter] postNotificationName: SSGameDataUpdatedFromiCloud object:nil]; |
postNotificationName:object:
will emit a notification through NSNotificationCenter
. All you need to do is catch it in your scene class and update the on-screen high score and pilot photo.
Now open MyScene.m and scroll towards the bottom of initWithSize:
. Find the [self setupHUD];
line. This is the right place to start observing for notifications about changes to the HUD. Add below that line:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGameData:) name:SSGameDataUpdatedFromiCloud object:nil]; |
You add the scene class as an observer for the SSGameDataUpdatedFromiCloud
notification and instruct NSNotificationCenter
to fire up didUpdateGameData:
if such a notification comes in. Looking good!
Being a good programmer, you’ll also want to remove the class from NSNotificationCenter
when it is destroyed. Add a dealloc
to the class implementation:
-(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:SSGameDataUpdatedFromiCloud object:nil]; } |
This should take care to keep NSNotificationCenter
sane and in balance.
And now – the grand finale! The method that will update the scene in real time! Yes, that means as changes come in!
Add this fabulous piece of code to wrap up iCloud integration:
-(void)didUpdateGameData:(NSNotification*)n { _highScore.text = [NSString stringWithFormat:@"High: %li pt", [RWGameData sharedGameData].highScore]; [self setupPilot]; } |
Nothing much to discuss in there — you update the high score label and invoke setupPilot
, which in turn updates the pilot photo.
That’s all there is to iCloud integration.
Now blow up some asteroids to celebrate your success!
To test whether iCloud is working, try this on your iCloud enabled iOS device:
- Launch the game and take a new photo, the play the game few times
- Then stop the game from Xcode
- Delete the game from your device
- Now re-launch the game from within Xcode
So what happens when you launch the game after you deleted the previous installation?
You start with a clean slate:
A few seconds into the game:
You got it working! Congrats! Time for a cold beer. :]
Where To Go From Here?
Here is the final version of the example project from this tutorial series.
You covered a lot in the second part of this tutorial by adding the following new features:
- Keychain integration and basic security against cheaters
- iCloud integration for your game
- Key-value storage for the player’s high score and photo
- Observing incoming changes from the cloud and updating the scene as they come in
Now you know a lot more about how to save your game data, but there’s always more to learn. Here are a few ideas to dive deeper into this subject:
- You didn’t get to display the total distance flown by the user. You can add a new label for that to the scene.
- How about keeping the last three (or more) scores for the player? This will give you an opportunity to use
encodeObject:forKey:
with anNSArray
instance
But there’s even more to the Space Shooter project than what you see here! Are you aware that Ray has developed the Space Shooter game into a full blown game starter kit? If you lay your hands on the starter kit, you get an epic, detailed tutorial, plus the code to a complete space game based on Space Shooter. Pretty cool, right?
Check out the Space Shooter starter kit – it comes with art, music, and source code; the kit extends the game by adding bad guys, power-ups, more weapons and level bosses.
If you have any questions or comments, please share in the comments below.
How to Save your Game’s Data: Part 2/2 is a post from: Ray Wenderlich
The post How to Save your Game’s Data: Part 2/2 appeared first on Ray Wenderlich.