Data Storage - iOS Game Development Cookbook (2014)

iOS Game Development Cookbook (2014)

Chapter 5. Data Storage

Games are apps, and apps run on data. Whether it’s just resources that your game loads or saved-game files that you need to store, your game will eventually need to work with data stored on the flash chips that make up the storage subsystems present on all iOS devices.

In this chapter, you’ll learn how to convert objects into saveable data, how to work with iCloud, how to load resources without freezing up the rest of the game, and more.

Saving the State of Your Game

Problem

You want game objects to be able to store their state, so that it can be loaded from disk.

Solution

Make your objects conform to the NSCoding protocol, and then implement encodeWithCoder: and initWithCoder:, like so:

- (void) encodeWithCoder:(NSCoder*) coder {

[coder encodeObject:self.objectName forKey:@"name"];

[coder encodeInt:self.hitPoints forKey:@"hitpoints"];

}

- (id) initWithCoder:(NSCoder*)coder {

// note: not [super initWithCoder:coder]!

self = [super init];

if (self) {

self.objectName = [coder decodeObjectForKey:@"name"];

self.hitPoints = [coder decodeIntForKey:@"hitpoints"];

}

}

When you want to store this information into an NSData object, for saving to disk or somewhere else, you use the NSKeyedArchiver:

GameObject* gameObject = ... // an object that conforms to NSCoder

NSData* archivedData = [NSKeyedArchiver archivedDataWithRootObject:gameObject];

// Save archivedData somewhere

If you have an NSData object that contains information encoded by NSKeyedArchiver, you can convert it back into a regular object with NSKeyedUnarchiver:

NSData* loadedData = ... // an NSData that contains archived data

GameObject* object = [NSKeyedUnarchiver unarchiveObjectWithData:loadedData];

Discussion

When you’re making objects able to save their state on disk, the first step is to figure out exactly what you need to save, and what can be safely thrown away. For example, a monster may need to save the amount of hit-points it has left, but may not need to save the direction in which its eyes are pointing. The less data you store, the better.

The NSKeyedArchiver class lets you store specific data in an archive. When you use it, you pass in an object that you want to be archived. This object can be any object that conforms to the NSCoding protocol, which many built-in objects in Cocoa Touch already do (such as NSString,NSArray, NSDictionary, NSNumber, NSDate, and so on). If you want to archive a collection object, such as an NSArray or NSDictionary, it must only include objects that conform to NSCoding.

NSKeyedArchiver gives you an NSData object, which you can write to disk, upload to another computer, or otherwise keep around. If you load this data from disk, you can un-archive the data and get back the original object. To do this, you use NSKeyedUnarchiver, which works in a very similar way to NSKeyedArchiver.

The encodeWithCoder: method is used by the archiving system to gather the actual information that should be stored in the final archive. This method receives an NSCoder object, which you provide information to. For each piece of info you want to save, you use one of the following methods:

§ encodeInt:forKey:

§ encodeObject:forKey:

§ encodeFloat:forKey:

§ encodeDouble:forKey:

§ encodeBool:forKey:

NOTE

These encoding methods are only the most popular ones. Others exist, and you can find the complete list in the Xcode documentation for the NSCoder class.

When you want to encode, for example, an integer named “hitpoints,” you call encodeInt:forKey: like so:

[coder encodeInt:self.hitpoints forKey:@"hitpoints"];

When you use encodeObject:forKey:, the object that you provide must be one that conforms to NSCoding. This is because the encoding system will send it an encodeWithCoder: method of its own.

NOTE

The encoding system will detect if an object is encoded more than one time and remove duplicates. If, for example, both object A and object B use encodeObject:forKey: to encode object C, that object will be encoded only once.

Decoding is the process of getting back information from the archive. This happens when you use NSKeyedUnarchiver, which reads the NSData object you give it and creates all of the objects that were encoded. Each object that’s been unarchived is sent the initWithCoder: method, which allows it to pull information out of the decoder.

You get information from the decoder using methods that are very similar to those present in the encoder (again, this is just a sampling of the available methods):

§ decodeObjectForKey:

§ decodeIntForKey:

§ decodeFloatForKey:

§ decodeDoubleForKey:

§ decodeBoolForKey:

Storing High Scores Locally

Problem

You want to store high scores in a file, on disk.

Solution

First, put your high scores in NSDictionary objects, and then put those dictionaries in an NSArray:

NSDictionary* scoreDictionary = @{@"score": @(1000), @"date":[NSDate date],

@"playerName": playerName};

// you would probably have more than one of these

NSArray* highScores = @[scoreDictionary];

Next, determine the location on disk where these scores can be placed:

NSURL* documentsURL = [[[NSFileManager defaultManager]

URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]

lastObject];

NSURL* highScoreURL = [documentsURL URLByAppendingPathComponent:

@"HighScores.plist"];

Finally, write out the high scores array to this location:

[highScores writeToURL:highScoreURL atomically:YES];

You can load the high scores array by reading from the location:

NSError* error = nil;

highScores = [NSArray arrayWithContentsOfURL:highScoreURL error:&error];

if (highScores == nil) {

NSLog(@"Error loading high scores: %@", error);

} else {

NSLog(@"Loaded scores: %@", highScores);

}

Discussion

When you’re saving high scores, it’s important to know exactly what information you want to save. This will vary from game to game, but common things include the score, the time when the score was earned, and any additional context needed for that score to make sense.

A very easy way to save information to disk is to put each score in an NSDictionary, and put each NSDictionary in an NSArray. You can then write that array to disk, or load it back.

An application in iOS is only allowed to read and write files that are inside the app’s sandbox. Each app is limited to its own sandbox and is generally not allowed to access any files that lie outside of it. To get the location of an app’s Documents folder, which is located inside the sandbox, you use the NSFileManger class to give you the NSURL. You can then construct an NSURL based on that, and give that URL to the array, using it to write to the disk.

You can then do the reverse to load the data from disk.

If you want to store your high scores online, you can use Game Center (see Making Leaderboards and Challenges with Game Center).

Using iCloud to Save Games

Problem

You want to save the player’s game in iCloud.

Solution

NOTE

To work with iCloud, you’ll need to have an active iOS Developer Program membership.

First, activate iCloud support in your app. To do this, select the project at the top of the Project Navigator, and switch to the Capabilities tab. Turn on the “iCloud” switch.

Saving the player’s game in iCloud really means saving game data. This means that we’ll assume that you’ve saved your file somewhere. In these examples, saveGameURL is an NSURL that points to the location of your saved game file, which you’ve already written out to disk.

First, you need to check to see if iCloud is available. It may not be; for example, if the user hasn’t signed in to an Apple ID or has deliberately disabled iCloud on the device. You can check to see if iCloud is available by doing the following:

if ([[NSFileManager defaultManager] ubiquityIdentityToken] == nil) {

// iCloud is unavailable. You'll have to store the saved game locally.

}

To put a file in iCloud, you do this:

NSString* fileName = @"MySaveGame.save"; // this can be anything

// Moving into iCloud should always be done in a background queue, because it

// can take a bit of time

NSOperationQueue* backgroundQueue = [[NSOperationQueue alloc] init];

[backgroundQueue addOperationWithBlock:^{

NSURL* containerURL = [[NSFileManager defaultManager]

URLForUbiquityContainerIdentifier:nil];

NSURL* iCloudURL = [containerURL URLByAppendingPathComponent:fileName];

NSError* error = nil;

[[NSFileManager defaultManager] setUbiquitous:YES itemAtURL:saveGameURL

destinationURL:iCloudURL error:&error];

if (error != nil) {

NSLog(@"Problem putting the file in iCloud: %@", error);

}

}];

To find files that are in iCloud, you use the NSMetadataQuery class. This returns information about files that have been stored in iCloud, either by the current device or by another device the user owns. NSMetadataQuery works like a search—you tell it what you’re looking for, register to be notified when the search completes, and then tell it to start looking:

NSMetadataQuery* _query; // Keep this around in an instance variable

_query = [[NSMetadataQuery alloc] init];

[_query setSearchScopes:[NSArray arrayWithObjects:NSMetadataQueryUbiquitous

DocumentsScope, nil]];

// Search for all files ending in .save

[_query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE '*.save'",

NSMetadataItemFSNameKey]];

// Register to be notified of when the search is complete

NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];

[notificationCenter addObserver:self selector:@selector(searchComplete)

name:NSMetadataQueryDidFinishGatheringNotification object:nil];

[notificationCenter addObserver:self selector:@selector(searchComplete)

name:NSMetadataQueryDidUpdateNotification object:nil];

[_query startQuery];

You then implement a method that’s run when the search is complete:

- (void) searchComplete {

for (NSMetadataItem* item in _query.results) {

// Find the URL for the item

NSURL* url = [item valueForAttribute:NSMetadataItemURLKey];

if ([item valueForAttribute:NSMetadata

UbiquitousItemDownloadingStatusKey] == NSMetadataUbiquitousItem

DownloadingStatusCurrent) {

// This file is downloaded and is the most current version

[self doSomethingWithURL:url];

} else {

// The file is either not downloaded at all, or is out of date

// We need to download the file from iCloud; when it finishes

// downloading, NSMetadataQuery will call this method again

NSError* error = nil;

[[NSFileManager defaultManager] startDownloading

UbiquitousItemAtURL:url error:&error];

if (error != nil) {

NSLog(@"Problem starting download of %@: %@", url, error);

}

}

}

}

NOTE

An NSMetadataQuery runs until it’s stopped. If you make a change to a file that the query is watching, you’ll receive a new notification. If you’re done looking for files in iCloud, you can stop the query using the stopQuery method:

[_query stopQuery];

When a file is in iCloud and you make changes to it, iCloud will automatically upload the changed file, and other devices will receive the new copy. If the same file is changed at the same time by different devices, the file will be in conflict. You can detect this by checking theNSMetadataUbiquitousItemHasUnresolvedConflictsKey attribute on the results of your NSMetadataQuery; if this is set to YES, then there are conflicts.

There are several ways you can resolve a conflict; one way is to simply say, “The most recent version that was saved is the current one; ignore conflicts.” To indicate this to the system, you do this:

// Run this code when an NSMetadataQuery indicates that a file is in conflict

NSURL* fileURL = ...; // the URL of the file that has conflicts to be resolved

for (NSFileVersion* conflictVersion in [NSFileVersion unresolvedConflict

VersionsOfItemAtURL:fileURL]) {

conflictVersion.resolved = YES;

}

[NSFileVersion removeOtherVersionsOfItemAtURL:fileURL];

Discussion

iCloud is a technology from Apple that syncs documents and information across the various devices that a user owns. “Devices,” in this case, means both iOS devices and Macs; when you create a document and put it in iCloud, the same file appears on all devices that you’re signed in to. Additionally, the file is backed up by Apple on the Web.

To use iCloud, you need to have an active iOS Developer account, because all iCloud activity in an app is linked to the developer who created the app. You don’t have to do anything special with your app besides have Xcode activate iCloud support for it—all of the setup is handled for you automatically.

It’s worth keeping in mind that not all users will have access to iCloud. If they’re not signed in to an Apple ID, or if they’ve deliberately turned off iCloud, your game still needs to work without it. This means saving your game files locally, and not putting them into iCloud.

Additionally, it’s possible that the user might have signed out of iCloud, and a different user has signed in. You can check this by asking the NSFileManager for the ubiquityIdentityToken, which you can store; if it’s different to the last time you checked, you should throw away any local copies of your saved games, and redownload the files from iCloud.

You should always try to perform iCloud work on a background queue. iCloud operations can frequently take several dozen milliseconds to complete, which can slow down your game if you run them on the main queue.

Using the iCloud Key/Value Store

Problem

You want to store small amounts of information in iCloud.

Solution

Use NSUbiquitousKeyValueStore, which is like an NSMutableDictionary whose contents are shared across all of the user’s devices:

NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];

// Get the most recent level

int mostRecentLevel = (int)[store longLongForKey:@"mostRecentLevel"];

// Save the level

[store setLongLong:13 forKey:@"mostRecentLevel"];

// Sign up to be notified of when the store changes

[[NSNotificationCenter defaultCenter] addObserver:self

selector:@selector(storeUpdated:)

name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:store];

// Elsewhere:

- (void) storeUpdated:(NSNotification*)notification {

NSArray* listOfChangedKeys = [notification.userInfo

objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];

for (NSString* changedKey in listOfChangedKeys) {

NSLog(@"%@ changed to %@", changedKey, [store objectForKey:changedKey]);

}

}

Discussion

Many games don’t need to store very much information in order to let the players keep their state around. For example, if you’re making a puzzle game, you might only need to store the number of the level that the players reached. In these cases, the NSUbiquitousKeyValueStore is exactly what you need. The ubiquitous key/value store stores small amounts of data—strings, numbers, and so on—and keeps them synchronized.

NOTE

You’ll need to activate iCloud support in your app for NSUbiquitousKeyValueStore to work.

Unlike when you’re working with files, conflict resolution in the ubiquitous key/value store is handled automatically for you by iCloud: the most recent value that was set wins. This can sometimes lead to problems. For example, consider the following user experience:

1. You have a puzzle game, and the highest level that’s been unlocked is stored in the key/value store.

2. You play up to level 6 on your iPhone, and iCloud syncs the key/value store.

3. Later, you play the game on your iPad, but it’s offline. You get up to level 2 on your iPad. Later, your iPad is connected to the Internet, and iCloud syncs this latest value. Because it’s the latest value to be set, it overwrites the “older” value of 2.

4. You then play the game on your iPhone, and are very surprised to see that your progress has been “lost.” You delete the app and leave a 1-star review on the App Store. The app developer goes bankrupt and dies alone in a gutter.

To solve this problem, you should keep data in the local user defaults, and update it only after comparing it to the ubiquitous store. When the store changes, compare it against the local user defaults; if the ubiquitous store’s value is lower, copy the value from the local store into the ubiquitous store, overwriting it. If it’s higher, copy the value from the ubiquitous store into the local store. Whenever you want to read the information, always consult the local store.

You’re limited to 1 MB of data in the ubiquitous key/value store on a per-application basis. If you try to put more data than this into the key/value store, the value will be set to nil.

Loading Structured Information

Problem

You want to store and load structured information (e.g., NSArray and NSDictionary objects), in a way that produces files that are easy to read and write.

Solution

Use the NSJSONSerialization class to read and write JSON files.

To create and write out a file:

NSDictionary* informationToSave = @{

@"playerName": @"Grabthar",

@"weaponType": @"Hammer",

@"hitPoints": 1000,

@"currentQuests": @[@"save the universe", @"get home"]

};

NSURL* locationToSaveTo = ... // where to save the information

NSError* error = nil;

NSData* dataToSave = [NSJSONSerialization dataWithJSONObject:informationToSave

options:0 error:&error];

if (error != nil) {

NSLog(@"Error converting data to JSON: %@", error);

}

[dataToSave writeToURL:locationToSaveTo atomically:YES];

The file created by the preceding code looks like this:

{

"playerName": "Grabthar",

"weaponType": "Hammer",

"hitPoints": 1000,

"currentQuests": [

"save the galaxy",

"get home"

]

}

You can load this file back in and convert it back to its original type, as well:

NSURL* locationToLoadFrom = ... // where to load the information from

NSError* error = nil;

NSData* loadedData = [NSData dataWithContentsOfURL:locationToLoadFrom

error:&error];

if (loadedData == nil) {

NSLog(@"Error loading data: %@", error);

}

NSDictionary* loadedDictionary = [NSJSONSerialization

JSONObjectWithData:loadedData options:0 error:&error];

if (loadedDictionary == nil) {

NSLog(@"Error processing data: %@", error);

}

// ALWAYS ensure that the data that you've received is the type you expect:

if ([loadedData isKindOfClass:[NSDictionary class]] == NO) {

NSLog(@"Error: loaded data is not what I expected!");

}

Discussion

JSON, which is short for JavaScript Object Notation, is a simple, easy-to-read format for storing structured information like dictionaries and arrays. NSJSONSerialization is designed to provide an easy way to convert objects into JSON data and back again.

Note that JSON can only store specific kinds of data. Specifically, you can only store the following types:

§ Strings

§ Numbers

§ Boolean values (i.e., true and false)

§ Arrays

§ Dictionaries (JSON calls these “objects”)

This means that NSJSONSerialization can only be given NSStrings, NSNumbers, NSArrays, and NSDictionary objects to process. If you don’t do this, then the class won’t produce JSON.

You can check to see if the object you’re about to give to NSJSONSerialization is able to be converted to JSON by using the isValidJSONObject method:

NSDictionary* myDictionary = @{@"canIDoThis": @(YES)};

BOOL canBeConverted = [NSJSONSerialization isValidJSONObject:myDictionary];

// canBeConverted = YES

Deciding When to Use Files or a Database

Problem

You want to decide whether to store information as individual files, or as a database.

Solution

Use individual files when:

§ You know that you’ll need the entire contents of the file all at the same time.

§ The file is small.

§ The file is easy to read and process, and won’t take lots of CPU resources to get information out of.

Use a database when:

§ The file is large, and you don’t need to load everything in at once.

§ You only need a little bit of information from the file.

§ You need to very quickly load specific parts of the file.

§ You want to make changes to the file while continuing to read it.

Discussion

Games tend to load files for two different reasons:

§ The file contains information that needs to be kept entirely in memory, because all of it is needed at once (e.g., textures, level layouts, and some sounds).

§ The file contains a lot of information, but only parts of it need to be read at once (e.g., monster information, player info, dialogue).

Databases are much faster and more efficient at getting small amounts of information from a larger file, but the downside is lots of increased code complexity.

Using SQLite for Storage

Problem

You want to use SQLite, a fast database system, for storing and loading information.

Solution

To work with SQLite, you first create a sqlite3 object. This represents your database, and you do all of your work using it.

You open a database using the sqlite3_open function:

const char* filename = ... // a C string containing a path to where you want

// the file to be

sqlite3* database = nil;

sqlite3_open(filename, &database);

When you’re done with the database, you close it with the sqlite3_close function:

sqlite3_close(database);

You interact with SQLite by creating SQL statements, using the sqlite3_prepare_v2 function. You then execute the statements by using the sqlite3_step function.

Data in SQLite databases is stored in tables. To create a table, you use the “CREATE TABLE” statement (note that this line has been broken here to fit within the page margins; you must enter it on a single line or a compiler error will be raised):

sqlite3_stmt* statement = nil;

const char* query = "CREATE TABLE IF NOT EXISTS monsters (id INTEGER

PRIMARY KEY, type TEXT, hitpoints INTEGER);"

sqlite3_prepare_v2(database, query, strlen(query), &statement, NULL);

if (sqlite3_step(statement) != SQLITE_DONE) {

// There was a problem

}

sqlite3_finalize(statement);

You can insert information into the table using the “INSERT INTO” statement:

sqlite3_stmt* statement = nil;

const char* typeName = "goblin";

int hitpoints = 45;

const char* query = "INSERT INTO monsters (type, hitpoints) VALUES (?, ?)";

sqlite3_prepare_v2(database, query, strlen(query), &statement, NULL);

sqlite3_bind_text(statement, 1, typeName, strlen(typeName), SQLITE_TRANSIENT);

sqlite3_bind_int(statement, 2, hitpoints);

if (sqlite3_step(statement) != SQLITE_DONE) {

// There was a problem

}

sqlite3_finalize(statement);

To get information from the database, you use the “SELECT” statement:

sqlite3_stmt statement = nil;

const char* typeName = "monster";

// Create the query

const char* query = "SELECT (type, hitpoints) FROM monsters WHERE type=?";

sqlite3_prepare_v2(database, query, strlen(query), &statement, NULL);

// Bind the monster type name we're searching for to the query

sqlite3_bind_text(statement, 1, typeName, strlen(typeName), SQLITE_TRANSIENT);

// For each row in the database, get the data

while (sqlite3_step(statement) == SQLITE_ROW) {

// Get the hitPoints, which is column 2 in the query we submitted

int hitPoints = sqlite3_column_int(statement, 2);

// Do something with hitPoints

}

sqlite3_finalize(statement);

When you create these kinds of statements, you first create the statement, and then you bind values to it.

WARNING

Never create statements like this (note that the second statement should appear all on one line, without wrapping):

NSString* typeName = @"monster";

NSString* query = [NSString stringWithFormat:@"SELECT (type, hitpoints)

FROM monsters WHERE type=\"%@\"", typeName];

You should always bind values to your statements, using the sqlite3_bind_ family of functions. These ensure that the types remain the same, and that the resulting SQL statement is valid. It leads to cleaner code, and prevents the security risk posed by SQL injection.

Discussion

SQLite is a public-domain library for working with databases. In SQLite, the databases are stored as local files—unlike in database systems like PostgreSQL or MySQL where you connect to a server, in SQLite all of your work is done on a file that’s kept on the local filesystem. This means that SQLite is very well suited for “private” use, where a single application uses the database.

Because SQLite databases are files, you can put them anywhere, including iCloud. Apple also provides special support for databases in iCloud, through Core Data, but that’s much more complicated, and we don’t have room to give it justice in this book.

Managing a Collection of Assets

Problem

Your game has a large number of big files, and you want to manage them in folders.

Solution

First, put all of your assets—your textures, sounds, data files, and so on—in a folder, which is inside the folder where you’re keeping your source code. Call this folder Assets.

Drag the folder into the Project Navigator. Turn off “Copy items into destination group’s folder,” and set the Folders option to “Create folder references for any added folders.”

Create a new class, called AssetManager. Put the following code in AssetManager.h:

#import <Foundation/Foundation.h>

typedef void (^LoadingBlock)(NSData* loadedData);

typedef void (^LoadingCompleteBlock)(void);

@interface AssetManager : NSObject

@property (strong) NSURL* baseURL;

+ (AssetManager*) sharedManager;

- (NSURL*) urlForAsset:(NSString*) assetName;

- (void) loadAsset:(NSString* )assetName withCompletion:(LoadingBlock)

completionBlock;

- (void) waitForResourcesToLoad:(LoadingCompleteBlock)completionBlock;

@end

And in AssetManager.m add the following:

#import "AssetManager.h"

static AssetManager* sharedAssetManager = nil;

@implementation AssetManager {

dispatch_queue_t _loadingQueue;

}

+ (AssetManager *)sharedManager {

// If the shared instance hasn't yet been created, create it now

if (sharedAssetManager == nil) {

sharedAssetManager = [[AssetManager alloc] init];

}

return sharedAssetManager;

}

- (id)init

{

self = [super init];

if (self) {

// Find assets inside the "Assets" folder, which is copied in

self.baseURL = [[[NSBundle mainBundle] resourceURL]

URLByAppendingPathComponent:@"Assets" isDirectory:YES];

// Create the loading queue

_loadingQueue = dispatch_queue_create("com.YourGame.LoadingQueue",

DISPATCH_QUEUE_SERIAL);

}

return self;

}

- (NSURL *)urlForAsset:(NSString *)assetName {

// Determine where to find the asset

return [self.baseURL URLByAppendingPathComponent:assetName];

}

- (void)loadAsset:(NSString *)assetName withCompletion:(LoadingBlock)

completionBlock {

// Load the asset in the background; when it's done, give the loaded

// data to the completionBlock

NSURL* urlToLoad = [self urlForAsset:assetName];

dispatch_queue_t mainQueue = dispatch_get_main_queue();

dispatch_async(_loadingQueue, ^{

NSData* loadedData = [NSData dataWithContentsOfURL:urlToLoad];

dispatch_sync(mainQueue, ^{

completionBlock(loadedData);

});

});

}

- (void)waitForResourcesToLoad:(LoadingCompleteBlock)completionBlock {

// Run the block on the main queue, after all of the load requests that

// have been queued up are complete

dispatch_queue_t mainQueue = dispatch_get_main_queue();

dispatch_async(_loadingQueue, ^{

dispatch_sync(mainQueue, completionBlock);

});

}

To use this code:

// In this example, there's a large file called "Spaceship.png" in the

// "Images" folder, which is in the Assets folder:

[[AssetManager sharedManager] loadAsset:@"Images/Spaceship.png"

withCompletion:^(NSData* loadedData) {

// Do something with the loaded data

}];

[[AssetManager sharedManager] waitForResourcesToLoad:^{

// This will be called after the image and any other resources have

// been loaded

}];

// In the meantime, continue running the game while we wait for the image

// to load

Discussion

In Xcode, a folder reference is a special kind of group whose contents are automatically updated when the files on disk change. Folder references are great for when you have a moderately complex folder structure that you’d like to preserve in your game.

NOTE

If you change the contents of the folder, it will automatically update in Xcode, but those changes won’t necessarily copy over when you build your game. To fix this, do a Clean action after changing your resources (choose Clean from the Product menu, or press Command-Shift-K).

Large files can take a long time to load, and you don’t want the player to be looking at a frozen screen while resources are loaded from disk. To address this, you can use a class that handles the work of loading resources in the background. The AssetManager class in this solution handles the work for you, by creating a new dispatch queue and doing the resource loading using the new queue.

If you want to load multiple resources and then run some code, you can use the waitForResourcesToLoad: method, which runs a block of code after the images have finished loading in the background.

Storing Information in NSUserDefaults

Problem

You want to store small amounts of information, like the most recently visited level in your game.

Solution

The NSUserDefaults class is a very useful tool that lets you store small pieces of data—strings, dates, numbers, and so on—in the user defaults database. The user defaults database is where each app keeps its preferences and settings.

There’s only a single NSUserDefaults object that you work with, which you access using the standardUserDefaults method:

NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];

Once you have this object, you can treat it like an NSMutableDictionary object:

// Store a string into the defaults

[defaults setObject:@"A string" forKey:@"mySetting"];

// Get this string back out

NSString* string = [defaults stringForKey:@"mySetting"];

You can store the following kinds of objects in the NSUserDefaults system:

§ NSData

§ NSNumber

§ NSString

§ NSDate

§ NSArray, as long as it only contains objects in this list

§ NSDictionary, as long as it only contains objects in this list

Discussion

When you store information into NSUserDefaults, it isn’t stored to disk right away—instead, it’s saved periodically, and at certain important moments (like when the user taps the home button). This means that if your application crashes before the information is saved, whatever you stored will be lost.

You can force the NSUserDefaults system to save to disk any changes you’ve made to NSUserDefaults by using the synchronize method:

[defaults synchronize];

Doing this will ensure that all data you’ve stored to that point has been saved. For performance reasons, you shouldn’t call synchronize too often—it’s really fast, but don’t call it every frame.

Information you store in NSUserDefaults is backed up, either to iTunes or to iCloud, depending on the user’s settings. You don’t need to do anything to make this happen—this will just work.

Sometimes, it’s useful for NSUserDefaults to provide you with a default value—that is, a value that you should use if the user hasn’t already provided one of his own.

For example, let’s say your game starts on level 1, and you store the level that your player has reached as currentLevel in NSUserDefaults. When your game starts up, you ask NSUserDefaults for the current level, and set up the game from there:

NSInteger levelNumber = [defaults integerForKey:@"currentLevel"];

However, what should happen the first time the player starts the game? If no value is provided for the currentLevel setting, the first time this code is called, you’ll get a value of 0—which is incorrect, because your game starts at 1.

To address this problem, you can register default values. This involves giving the NSUserDefaults class a dictionary of keys and values that it should use if no other value has been provided:

[defaults registerDefaults:@{@"currentLevel": @1}];

NSInteger levelNumber = [defaults integerForKey:@"currentLevel"];

// levelNumber will be either 1, or whatever was last stored in NSUserDefaults.

WARNING

It’s very, very easy for users to modify the information you’ve stored in NSUserDefaults. Third-party tools can be used to directly access and modify the information stored in the defaults database, making it very easy for people to cheat.

If you’re making a multiplayer game, for example, and you store the strength of the character’s weapon in user defaults, it’s possible for players to modify the database and make their players have an unbeatable weapon.

That’s not to say that you shouldn’t use NSUserDefaults, but you need to be aware of the possibility of cheating.

Implementing the Best Data Storage Strategy

Problem

You want to make sure your game stores data sensibly, and doesn’t annoy your users.

Solution

The solution here is simple: don’t drop data. If your game can save its state, then it should be saving its state. You can’t expect the user to manually save in an iOS game, and you should always persist data at every available opportunity.

Discussion

Nothing is more annoying than losing your progress in a game because a phone call came in. Don’t risk annoying your users: persist the state of the game regularly!

In-Game Currency

Problem

You want to keep track of an in-game resource, like money, which the player can earn and spend.

Solution

The requirements for this kind of thing vary from game to game. However, having an in-game currency is a common element in lots of games, so here’s an example of how you might handle it.

In this example, let’s say you have two different currencies: gems and gold. Gems are permanent, and the player keeps them from game to game. Gold is temporary, and goes away at the end of a game.

To add support for these kinds of currencies, create a class that manages their storage:

@interface CurrencyManager

@property (nonatomic, assign) NSInteger gems;

@property (nonatomic, assign) NSInteger gold;

- (void) endGame;

@end

@implementation CurrencyManager

@dynamic gems;

- (void) setGems:(NSInteger)gems {

// Set the updated count of gems in the user defaults system

[[NSUserDefaults standardUserDefaults] setInteger:gems forKey:@"gems"];

}

- (void) gems {

// Ask the user defaults system for the current number of gems

return [[NSUserDefaults standardUserDefaults] integerForKey:@"gems"];

}

- (void) endGame {

// Game ended; get rid of all of the gold (but don't do anything to gems)

self.gold = 0;

}

@end

Discussion

In this solution, the gems property stores its information using the NSUserDefaults system, rather than simply leaving it in memory (as is done with the gold property).

When you create a property, Objective-C creates an instance variable in which the information is stored. However, if you mark a property as @dynamic, the instance variable won’t be created, and the setter and getter methods for the property need to handle the data storage themselves.

In this solution, the information is stored in NSUserDefaults. From the perspective of other objects, the property works like everything else:

CurrencyManager* currencyManager = ...

currencyManager.gems = 50;

currencyManager.gold = 100;

When data is stored in user defaults, it persists between application launches. This means that your gems will stick around when the application exits—something that players will appreciate. Note that data stored in the user defaults system can be modified by the user, which means cheating is not impossible.