March 23rd, 2016
Filed under: Game Development, iOS Development, Mac Development | 11 comments
I reached a point in developing LetterMiner where I wanted to save the game when the player quit so they could pick up where they left off. I didn’t find a lot of information online on saving data in SpriteKit games so I’ve decided to share what I’ve learned. I hope it helps other people.
To save data with NSCoding
in Swift, that data must be in Swift classes. Swift structs don’t conform to the NSCoding
protocol. If you want to use Swift structs in your game, create a SaveGame
class with properties for each piece of data you want to save and do all your data reading and writing in that class.
The following example shows a simple Player
class that conforms to the NSCoding
protocol:
class Player: NSObject, NSCoding {
var name = "Player 1"
var score = 0
}
You noticed my Player
class also inherits from NSObject
. NSObject
is the base class for the Cocoa and Cocoa Touch frameworks. If you have a class that is going to work with Apple’s frameworks, it’s usually good to inherit from NSObject
.
To write data the class must implement the method encodeWithCoder
. Call the NSCoder
method encodeObject
to save a piece of data. The following code shows an example of writing data for the Player
class:
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(name, forKey: "name")
coder.encodeObject(score, forKey: "score")
}
Swift 3 Code
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(score, forKey: "score")
}
Make sure you remember the key you use when writing the data. You must use the same key to read the data.
I’ve kept the example simple with one string property and one integer property. You can also write structs, classes, and arrays with the encodeObject
method. NSCoder
also has methods to write primitive data types like integers and floating-point numbers. What I like about encodeObject
is it works on any data type.
To read data the class must implement the initWithCoder
method. Call the NSCoder
method decodeObjectForKey
to read the data. The following code shows an example of reading data for the Player
class:
required convenience init(coder decoder: NSCoder) {
self.init()
name = decoder.decodeObjectForKey("name") as! String
score = decoder.decodeObjectForKey("score") as! Int
}
Swift 3 Code
required convenience init(coder decoder: NSCoder) {
self.init()
name = decoder.decodeObject(forKey: "name") as! String
score = decoder.decodeInteger(forKey: "score")
}
In Swift the initWithCoder
method is an init
method that takes a coder as an argument.
Saving the data to a file requires the following steps:
NSMutableData
.NSKeyedArchiver
.NSKeyedArchiver
method encodeObject
to save each piece of data you want to save.NSKeyedArchiver
method finishEncoding
when you’re finished.NSMutableData
method writeToURL
to save the data to a file.The following code shows an example of saving a game:
var player: Player
func saveGame(notification: NSNotification) {
let saveData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: saveData)
archiver.encodeObject(player.name, forKey: "name")
archiver.encodeObject(player.score, forKey: "score")
archiver.finishEncoding()
let saveLocation = saveFileLocation()
_ = saveData.writeToURL(saveLocation, atomically: true)
}
Swift 3 Code
func saveGame(notification: NSNotification) {
let saveData = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: saveData)
archiver.encode(player.name, forKey: "name")
archiver.encode(player.score, forKey: "score")
archiver.finishEncoding()
let saveLocation = saveFileLocation()
_ = saveData.write(to: saveLocation, atomically: true)
}
Your saveGame
method doesn’t have to take a notification
argument. I use the argument because I want my game to save when the player quits the game. The following code lets you register for the notification when the player quits a Mac game:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveGame:",
name:NSApplicationWillTerminateNotification, object:nil)
Swift 3 Code
NotificationCenter.default.addObserver(self,
selector:#selector(GameScene.saveGame(_:)),
name:NSNotification.Name.NSApplicationWillTerminateNotification, object:nil)
The Swift 3 code assumes the saveGame
method is in a GameScene
class. If your saveGame
method is in another class, substitute the name of that class.
For an iOS game the notification to register for is UIApplicationDidEnterBackgroundNotification
.
Notice the colon at the end of the saveGame:
selector. Don’t forget to add the colon, or you’ll spend a lot of time figuring out why the notification never fires.
I will talk about the saveLocation
variable later in the article.
Loading data from a file requires the following steps:
NSData
.NSKeyedUnarchiver
.NSKeyedUnarchiver
method decodeObjectForKey
to load each piece of data.NSKeyedUnarchiver
method finishDecoding
when you’re finished.The following example shows an example of loading a saved game:
var player = Player()
func loadGame() {
let saveLocation = saveFileLocation()
if let saveData = NSData(contentsOfURL: saveLocation) {
let unarchiver = NSKeyedUnarchiver(forReadingWithData: saveData)
player.name = unarchiver.decodeObjectForKey("name") as! String
player.score = unarchiver.decodeObjectForKey("score") as! Int
unarchiver.finishDecoding()
}
}
Swift 3 Code
func loadGame() {
let saveLocation = saveFileLocation()
if let saveData = try? Data(contentsOf: saveLocation) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: saveData)
player.name = unarchiver.decodeObject(forKey: "name") as! String
player.score = unarchiver.decodeInteger(forKey: "score")
unarchiver.finishDecoding()
}
}
Mac games should place saved game files in the Application Support folder. You will have to create a folder with the name of your game in the Application Support folder and place the saved game files in the folder you create. You can also place iOS saved games files in the Application Support folder, but you can place them in the Documents folder if you want.
The NSFileManager
class has methods to find and create directories. Call the URLForDirectory
method to find the Application Support folder. The following code finds the user’s Application Support directory:
do {
let gameSaveDirectory = try fileManager.URLForDirectory(
.ApplicationSupportDirectory, inDomain: .UserDomainMask,
appropriateForURL: nil, create: true)
} catch {
// Handle any errors here
}
Swift 3 Code
do {
let gameSaveDirectory = try fileManager.url(
for: .ApplicationSupportDirectory, in: .UserDomainMask,
appropriateFor: nil, create: true)
} catch {
// Handle any errors here
}
The URLForDirectory
function throws an error in Swift 2. That’s why the code has to be wrapped in a do
block.
At this point you have access to the Application Support directory. The first time you save you must create a directory for your game inside the Application Support folder. Call the createDirectoryAtURL
method to create a directory.
let myGamePath = gameSaveDirectory.URLByAppendingPathComponent("MyGame")
do {
// Create a directory to save games the first time we save.
try fileManager.createDirectoryAtURL(myGamePath,
withIntermediateDirectories: true, attributes: nil)
} catch {
// Handle any errors here
}
Swift 3 Code
let myGamePath = gameSaveDirectory.appendingPathComponent("MyGame")
do {
// Create a directory to save games the first time we save.
try fileManager.createDirectory(at: myGamePath,
withIntermediateDirectories: true, attributes: nil)
} catch {
// Handle any errors here
}
Now you’re in your game’s Application Support folder. The final step is to create a file name for the saved game. The NSURL
method URLByAppendingPathComponent
lets you add the file name to the path you’ve built.
let savePath = myGamePath.URLByAppendingPathComponent("MySavedGame.savegame")
Swift 3 Code
let savePath = myGamePath.appendingPathComponent("MySavedGame.savegame")
Now you have a URL you can use to save games and load saved games. In my saveGame
and loadGame
functions I referred to a saveLocation
variable that was the result of calling a function called saveFileLocation
. The savePath
variable is what saveFileLocation
returns.
Is this written backwards, on purpose? Each thing reads like it’s the thing that follows, not the code shown. encode, in the first step, only prepares for saving, it doesn’t save. etc… right?
Encode does not do the actual save. You’re right there. Encode encodes the object so it can be saved.
I started with encoding and decoding because you can’t save and load game data until your classes are set up to encode and decode data.
Can you provide a github repo of a working copy of this sample?
Where does loadGame() and saveGame() functions go? In the same class as the Player class or somewhere else?
AD,
I don’t have a simple game saving example to put on GitHub. Sorry.
Most games have a Game class as the main class in the game. The loadGame() and saveGame() functions would go there. If the only game data you’re saving is player data, you could load and save the game in the Player class. But most games need to save more data than player data. You should load and save the game in a class that has access to everything you need to save.
I’ve built a simple outline for a game using arrays, but now I realize i should have created the save/load states first.
I’ve been using realm, but realized that the amount of data being saved is very low and realm seems too heavy for my requirements; plus i’d have to change a lot of my code
I think I understand what you mean by putting the loadGame() and saveGame() in another class; perhaps a game model class.
thanks
Lets say I want to save array of books;
var author:Author = Author(name: "Tom Clancy", books: nil)
let book0 = Book(title: "The Hunt for Red October", author: author)
let book1 = Book(title: "Red Storm Rising", author: author)
let books = [book0, book1]
author.books.append(contentsOf: books)
How do I save the books array?
How do I load the books array?
To save the books array you create an NSKeyedArchiver object. You call the archiver’s encode() function to save the books array. Something like the following:
archiver.encode(books, forKey: "books")
To load the books array create an NSKeyedUnarchiver object. Call the unarchiver’s decode() function to load the books array. Something like the following code:
books = unarchiver.decodeObject(forKey: "books") as! [Book]
Take a look at the Saving Data to a File and Loading Data from a File sections in the article. The code is similar. Instead of encoding and decoding a single value you’re encoding and decoding an array. Give your books array a key. The fact that you have an array doesn’t have any effect on the archiving and unarchiving.
Thanks for the answer. I would like to use the NSCoding to save relationships between different classes, such as one to many relationships.
I’ve been able to get a working solution on github, however I am unsure if I got it right.
As I understand it, the parent encodes its array of children, but
the child does not attempt to encode the parent and it required to be a weak reference so that it does not stay in retain cycle. Further, when reading it should add the children.
Anyhow, is this the correct way to do this?
Github:
https://github.com/worchyld/NSCodingParentChild/blob/master/NSCodingParentChild.playground/Contents.swift
I don’t see any problems with your code, but I have never tried saving parent child relationships with NSCoding so I can’t tell if what you did is the best way to save the data.
If you are dealing with one to many relationships, you should look into Core Data. Core Data was designed to handle relationships between different classes.
just thinking about security of the user’s data… is there an easy way to keep this encrypted without getting the user involved in entering passwords and so forth?
Dave,
I store the data locally on the user’s device so I haven’t had to worry about encrypting data to store in the cloud.
Sorry about the delay in my response. I wasn’t getting email notifications I normally get when someone comments on a post.