In one scene, I have this code:
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setInteger(score, forKey: "scoreKey")
defaults.synchronize()
When the user makes contact with the gap, the code runs:
score++
If the user hits an obstacle, the GameOverScene takes over. Here's the code I have for the GameOverScene to move the score to scene to scene:
let defaults = NSUserDefaults.standardUserDefaults()
let score = defaults.integerForKey("scoreKey")
scoreLabel.text = "\(score)"
However, there's a bug in my code where the scoreLabel doesn't update its text. For example, let's say a user scores 1 and dies. When he dies, the gameOverScene will come up and say that the score was 1. Then, lets say the user clicks restart, and scores 5 and then dies. In the GameOverScene, the scoreLabel will say 1.
Please help me!
You don't need to really call synchronise anymore if you use iOS 8 or above. This is recommended by Apple, yet a lot of people still do it. So get rid of that line if you still use it.
My preferred way for game data is using a singleton GameData class with NSCoding. No need to add variables all over the place and much cleaner. I advise you reading this.
http://www.raywenderlich.com/63235/how-to-save-your-game-data-tutorial-part-1-of-2
You can also integrate iCloud key value storage that way, it is very easy as its similar to user defaults (see my example below)
Anyway to start you off here is a simple example of how this could look.
import Foundation
/// Keys
private struct Key {
static let encodedData = "encodedData"
static let highScore = "highScore"
}
class GameData: NSObject, NSCoding {
// MARK: - Static Properties
/// Shared instance
static let shared: GameData = {
if let decodedData = UserDefaults.standard.object(forKey: Key.encodedData) as? GameData {
return gameData
} else {
print("No data, creating new")
return GameData()
}
}
// MARK: - Properties
/// Defaults
private let localDefaults = UserDefaults.standard
private let iCloudDefaults = NSUbiquitousKeyValueStore.default()
/// Progress (not saved, no need for saving the score because of the highScore var. Still have here for global single access)
var score = 0
/// Progress (saved)
var highScore = 0
// MARK: - Init
private override init() {
super.init()
print("GameData init")
NotificationCenter.default.addObserver(self, selector: #selector(updateFromCloud), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: iCloudDefaults)
iCloudDefaults.synchronize()
}
// MARK: - Convenience Init
convenience required init?(coder decoder: NSCoder) {
self.init()
print("GameData convenience init")
// Progress
highScore = decoder.decodeInteger(forKey: Key.highScore)
}
// MARK: - Encode
func encodeWithCoder(encoder: NSCoder) {
// Progress
encoder.encodeInteger(highScore, forKey: Key.highScore)
// MARK: - User Methods
/// Save
func save() {
if score > highScore {
highScore = score
}
saveLocally()
saveToCloud()
}
// MARK: - Internal Methods
/// Save locally
private func saveLocally() {
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(self)
localDefaults.setObject(encodedData, forKey: Key.encodedData)
}
/// Save to icloud
private func saveToCloud() {
print("Saving to iCloud")
// Highscores
if (highScore > iCloudDefaults.objectForKey(Key.highScore) as? Int ?? Int()) {
iCloudDefaults.setObject(highScore, forKey: Key.highScore)
}
/// Update from icloud
func updateFromCloud() {
print("Updating from iCloud")
// Highscores
highScore = max(highScore, iCloudDefaults.object(forKey: Key.highScore) as? Int ?? Int())
// Save
saveLocally()
}
Now in any scene if you want to use the score or saved highScore property you for example could say
GameData.shared.score++
or
scoreLabel.text = "\(GameData.shared.score)"
highScoreLabel.text = "\(GameData.shared.highScore)"
All your text labels will be updated immediately if you transition to a new scene or update the .text property. No need for userDefault sync etc.
Calling ...shared... will also initialise the helper. If you want to load gameData as soon as your game has launched you could call
GameData.shared
in your appDelegate or viewController. Its probably not really needed but your could still do it just to ensure the helper is initialised as soon as the game is launched.
If you want to save you call
GameData.shared.save()
Just remember to reset the score back to 0 in your gameScene.swift in the ViewDidLoad method.
GameData.shared.score = 0
This should make your life much easier. If you want to use iCloud all you have to do is go to your target and capabilities and turn on iCloud and tick keyValueStorage (not core data). Done.
Note:
To take it even a step further you could get the KeychainWrapper helper from JRendel on gitHub. Than instead of using NSUserDefaults to store the encoded gameData you use keychain, its dead simple to use.
You can save your score like below code:
NSUserDefaults.standardUserDefaults().setInteger(score, forKey: "scoreKey")
Then you can get your score was saved like below code:
if NSUserDefaults.standardUserDefaults().objectForKey("scoreKey") != nil
{
score = NSUserDefaults.standardUserDefaults().objectForKey("scoreKey") as! Int
}
scoreLabel.text = "\(score)"
To answer the question from your comment about using global structure...
According to docs :
Global variables are variables that are defined outside of any
function, method, closure, or type context.
Means you should define your struct right after the import statements at the top of the any file.
You make a structure like pointed from the link I've posted in comments, like this (I've placed the struct definition inside the GameScene.swift file):
struct GlobalData
{
static var gold = 0;
static var coins = 0;
static var lives = 0;
static var score = 0;
}
This struct will be available in GameOverScene as well. So, before transition, you will do something like :
GlobalData.score = 20
GlobalData.coins = 10
//etc.
And in your GameOverScene you will access it like this:
scoreNode.text = String(GlobalData.score)//scoreNode is SKLabelNode