Spritekit: Why Does It Wait One Round for the Score to Update? (Swift)

Saving a score in Spritekit swift

NSUserDefaults is the right tool for saving game score. It allows you to save data on persistent storage. So the next time the user runs your game, that data will be available.

Saving

var jellyBeanValue = 3
NSUserDefaults.standardUserDefaults().setInteger(3, forKey: "jellyBeanValue")

Loading (even after the app has been restarted)

var jellyBeanValue = NSUserDefaults.standardUserDefaults().integerForKey("jellyBeanValue")

More types

You can save/load other kinds of data with the following methods

func setBool(value: Bool, forKey defaultName: String)
func boolForKey(defaultName: String) -> Bool

func setFloat(value: Float, forKey defaultName: String)
func floatForKey(defaultName: String) -> Float

func setDouble(value: Double, forKey defaultName: String)
func doubleForKey(defaultName: String) -> Double

Should you need to save instances of your custom classes you can use

func setObject(value: AnyObject?, forKey defaultName: String)
func objectForKey(defaultName: String) -> AnyObject?

but in this case please take a look at this.

iCloud

NSUserDefaults saves data on the persistent storage of your device. If you want to synch your data among all the Apple devices where the user is signed in with a specific AppleID you should use iCloud.

Using a timer vs update to run game SpriteKit

First I think you are taking the FPS problem the wrong way around. You cannot "force" a faster frame rate than the device can give you. If you are basing the movements in your game on the assumption that every frame will be consistent, you are doing it wrong. It's actually how they did in the early days because CPUs were so slow and the difference from one generation to the new one wasn't too bad at first. But running an old DOS game on younger hardware will be tricky because the framerate is so high that the whole game mechanic becomes unstable or simply too fast to be playable.

The concept here is to think "over time" and to scale down any action in relation with the time elapsed between two frames.

The update() method gives you that opportunity by providing the current system clock state every frame. By keeping track of the time on the last frame, you can calculate the time difference with the current frame and use that difference to scale down what you are doing.

Using a timer to get the update on a consistent frame rate is not recommended nor practical. You may be calling the update closure at a given time interval, but the code inside that closure is taking time to execute on its own, and depending on your game logic, it might even have different execution times. So maybe the pause timing is consistent, but the code running before and after that pause might not be consistent. Then what happens if you run your game on a slower CPU? The code speed will change even more, making your timing inaccurate.

Another point against using an SKAction for your game loop is simply what they are. An action is an object in memory, meany to be reused by multiple objects. If you are making a "jump" action, for example, it is recommended to store that action somewhere and to reuse the same object every time you need something that "jumps", no matter what node it is. Your game loop is meant to be executed every frame, but not by different objects. Actions are also disposable. Meaning that you can kill an action even while it's running. If you put your game loop in an action, it will probably be run by the SKScene. If you use another action on your scene it becomes a puzzle right away because there are only two ways of removing an action besides letting it come to term: removing all actions or creating the action with an identifier key and use that key to remove any action with that key. If you don't see it already, it then forces you to put identifiers on every action that will be run by the scene and remove them one by one. And them again it leave a door open for a mistake that will get rid of your game loop because, keep it in mind, actions are DISPOSABLE! Then there is also no guarantee that your action will get executed first every single frame.

Why use the update() method? Simply because it is built IN your scene. No matter what, every frame, update() gets called first. THEN, the actions get executed. You cannot flush the update() method accidentally like you can with an action. You don't have to be careful about strong/weak relationships causing memory leaks because you are referring to objects from inside a closure like you do with an action.

Suggested reads:

  1. SKAction API reference
  2. SKScene API reference : read about the frame processing in SpriteKit. It will help you understand how they put everything together at every frame.

I hope it makes things clearer.

Swift: How to update a label every second (or shorter) with a timer?

Timers should be avoided with SpriteKit unless you are working with technologies outside of SpriteKit. The reason is because SpriteKit has its own time management, and a timer may not coincide with it. Example, in your case, timer updates score. What if you pause the game? Your timer still adds score unless you remember to stop it.

Instead, rely on the actions to do what you need Below is how you do what you are looking for.

let wait = SKAction.wait(forDuration: 0.5)
let update = SKAction.run(
{
seconds += 0.05
seconds = (seconds * 100).rounded() / 100
score = score + 0.015
updateScore()
}
)
let seq = SKAction.sequence([wait,update])
let repeat = SKAction.repeatForever(seq)
run(repeat)

@RonMyschuk mentioned a thought that I have missed, so I will add it in my answer as well.

If you need to create timers that you can individually pause or pause in a group, then add in extra SKNodes and attach the timer action to the node instead of the scene. Then when you need to pause the timer, you can just pause the node. This will allow your scene update to continue while your timers are paused.

How do I transfer the user's score to another scene in Swift and SpriteKit?

You can use NSUserDefaults class as an easiest solution...

In your GameplayScene you set score into persistent storage.

let defaults = NSUserDefaults.standardUserDefaults()
defaults.setInteger(score, forKey: "scoreKey")

defaults.synchronize()

Later in GameOver scene, you read persistent storage like this:

let defaults = NSUserDefaults.standardUserDefaults()
let score = defaults.integerForKey("scoreKey")
println(score)

About synchronize() method (from the docs):

Because this method is automatically invoked at periodic intervals,
use this method only if you cannot wait for the automatic
synchronization (for example, if your application is about to exit) or
if you want to update the user defaults to what is on disk even though
you have not made any changes.

Or I guess you can make a public property (score) on a GameOver scene, and when transitioning, to set that property (from a Gameplay scene) with a current score.

Similarly, you can set a value to userData property which every node has, like this:

 newScene.userData?.setValue(score, forKey: "scoreKey")

EDIT:

NSUserDefaults would be a preferred way if you are interested into a persistence (making a value available between app launches). Otherwise, you can use userData or a struct like pointed by KnightOfDragon in his example.

Using NSCoding to get High Score and LifeTime Score in Swift / SpriteKit

The reason you get a crash is because decoding objects for keys does not provide a default value if no key is found (e.g decodeBoolForKey defaults to false)

Therefore you need to adjust your code. As the other member also said, change the code to

let lifeTimeScore = aDecoder.decodeIntegerForKey(PropertyKey.lifeTimeScoreKey)

which will automatically set a default value if no key is found because it knows you are trying to decode an integer.

If you want use decodeObjectForKey you need to do some nil check first because there is no default value because you are decoding "AnyObject". The line

... as! Int

will force unwrap the objectForKey as an Int, but it might not be there and is therefore nil and than you get the crash. Either change it too

if aDecoder.decodeObjectForKey(PropertyKey.lifeTimeScoreKey) != nil {
let lifeTimeScore = aDecoder.decodeObjectForKey(PropertyKey.lifeTimeScoreKey) as! Int
}

or use this handy feature to put it in 1 line

 let lifeTimeScore = aDecoder.decodeObjectForKey(PropertyKey.lifeTimeScoreKey) as? Int ?? Int()

Here it will check if a key exists (as? Int) and if it doesnt it will create a new default one (?? Int()).

Most likely the best way is to use IntegerForKey because your "lifeTimeScore" is an Int, only use ObjectForKey when you have too.

How to get different random delays in a SpriteKit sequence?

let wait = SKAction.wait(forDuration: getRandomDelay())
let sequence = SKAction.sequence([runObstSwitch, wait])

creates the wait action once, which is then used in the sequence,
so the same amount of idle time is spent between the runObstSwitch
actions.

If you want the idle time to be variable, use
wait(forDuration:withRange:) instead. For example with

let wait = SKAction.wait(forDuration: 1.5, withRange: 2.0)
let sequence = SKAction.sequence([runObstSwitch, wait])

the delay will be a random number between 1.5-2.0/2 = 0.5 and 1.5+2.0/2 = 2.5 seconds, varying for each execution.

How to save game progress in a SpriteKit game

Make sure you are conforming to the NSCoding protocol correctly, should look like this

class Entity : GKEntity,NSCoding
{
func encodeWithCoder(aCoder: NSCoder)
{

}
required init?(coder aDecoder: NSCoder)
{

}
}

class Component : GKComponent,NSCoding
{
func encodeWithCoder(aCoder: NSCoder)
{

}
required init?(coder aDecoder: NSCoder)
{

}
}


Related Topics



Leave a reply



Submit