3
votes

In my sprite-Kit game, my game goes through the various scenes, across which I want certain variables to be available. See the following diagram:

enter image description here

So the title screen loads the main game screen. When the 'wave' is finished (all objective complete or all lives lost), the end scene is presented with some stats. Then if the player has lives left the main game is loaded again, if not the title screen is presented again.

I want the player's score and number of lives left to be available across the main game scene and the end scene; the high score should be kept across all scenes.

I'm using global variables for these, whereas most of my variables are properties defined under the class.

The first time I had to do this (for my score property before I added a high score or multiple lives) I used a score property on every scene and passed it between scenes when I did the presentScene for the next scene, but that seemed clumsy, especially when I had to add more variables.

Is there a best practice for this i.e. should global variables be used/not be used and if not, is there a correct way to handle properties, perhaps controlled by where you initialise them e.g. in init compared to didMoveToView, which determines if they are re-initiialised when the scene is shown again?

EDIT:

Am I doing something 'wrong' when I go from the End Wave Scene back into the Main Game Screen that is causing the main screen to be re-initialised again? Is there a way to preserve it (with all it's properties) so it isn't fully re-initialied but it's didMoveToView is called again?

3
This may be what you're looking for thatthinginswift.com/singletons - 0x141E
I use a Class in sharedIstance, because use NSCoding and iCloud saves - Simone Pistecchia
Still trying to work through these answers & comments to work out what I'm doing! - Steve Ives

3 Answers

6
votes

General rule of thumb: If you can avoid global variables, do so.

Now every node has a mutable dictionary called userData. This is designed to be used to save data to a node.

Upon creation of your new scene, transfer whatever data you need from the old scene.

let scene = SKScene(fileNamed:....)
scene.userData["highscore"] = self.highscore
view.presentScene(scene)

Now in the new scenes didMove(view:) method, you read from said userData

As 0x141E has pointed out, remember you need to initialize userData, since chances are you have a custom class, you could do this in your init methods, just remember to override all designated inits so that you have access to the convenience methods.

Also, as mentioned in the comments between Whirlwind and myself, SKNode is NSCoding compliant, so when you go and save your information, you can save the SKNode as a whole and userData will go along with it.

This means all you would have to do is write an extension for SKNode to save/load the node, and this avoids having to worry about managing another class.

6
votes

I know that many people work with generalized rules like "Don't use globals" or "Singletons are evil". These words of wisdom may be applicable at times, however another way of looking at them would be as tools to make you more efficient in getting your game finished.

There are times when globals/singletons are useful. Games and embedded systems play by different rules than conventional applications. These are typically resource constrained "applications" coupled with performance requirements. Globals are more common than you think in each. Singletons are also commonly in use. I've worked on both embedded systems and games and can say that on every project I've worked on, they've employed either globals, singletons, or both.

In practice, I use singletons over globals. Mainly because I don't have to worry about where to shove the global and what header file it belongs in. Note, the basis as to the why will differ from all you Swifties. For me this stuff is done in primarly in C++ with some Obj-C and a smattering here and there of Swift.

The need to manage game lifecycle is independent of SpriteKit. Many of the SpriteKit posts I see on SO have the devs embedding game state into the scene or VC. They are usually faced with some dilemma on how to move state to the next scene. While this approach may be good for what I will call "local state" (ie. state for the current screen, stage, etc), it is not good for global game state. For example, if you transition from a main screen to an option screen then back to main and then in the game, how are you tracking the changes to those options (eg. changing game difficulty)? Yes you certainly can pass around dictionaries, structs, or what have you around. But at some point of time, you are going to discover that having one common and convenient dumping ground for it all is more convenient. This "dumping ground" would be globals/singletons.

Now before ya'll start yelling at me, there is a catch to all this crazy talk of globals and singletons. I'm not saying create alot of globals/singletons. Rather, be controlled aboutwhen/if you employ globals/singletons.

I'm not talking a gazillion if 'em. I'm talking like one (although I usually have a handful in use). Enter our ficticious friend, the GameManager.

Let's walk down a very simple scenario of a space shooter. Say I have multiple screens such as the title screen, main screen, the in game screen, and the end wave screen. There are certain bits of information that are useful to be passed to the different screens. For example high score or even current score. Those may be values that you want to display on various screens/scenes. So where do you store the score? How do I pass the score between them? For example, if the last high score is displayed on all screens, where do I keep that value?

class GameManager : NSObject {
    // Overall game state, GameState is probably some enum
    var state:GameState
    var lastState:GameState

    // Sometimes tracking scenes is useful
    var currentScene:SKScene?
    var nextScene:SKScene? // Sometimes helpful if you need to construct the next scene and it takes a non-trivial amount of time

    // These are the globals we need to share to different "parts" of the game
    var score: UInt
    var highscore: UInt
    var lives: Uint
    var stage: Uint

    var highscores:[Uint]   // A list of all the high score for the user. You would probably want HS based on other users too

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

If this was a global, it would have some global definition of the instance outside any scope. Like:

var theGameManager : GameManager

So if you want to udpate the score, you'd do

theGameManager.score += 100

If it was a singleton updating the score would look something like:

GameManager.sharedInstance.score += 100

Okay the singleton syntax is a little longer, but perhaps a little less cryptic than the global as to where/what this is.

But now here is where you can have a little more power in things. Let's say when you add a score, every multiple of 100000 scores you an extra life. I can now easily leverage the setter and use that setter to also reward the extra life.

For example this:

GameManager.sharedInstance.score += 100 // the magic of the 100000 for extra life happens automagically in the setter

versus something potentially like this:

myScore += 100

// Do logic to find out if we get an extra life. 
// Is this done in a method and if so where does this method live?
// Or is it straight up code which will be duplicated?

Code like this which helps manage global game state is all neatly contained within the GameManager. In other words, the GameManager doesn't just maintain values, but it also provides a way of encapsulating functionality around those values.

Hmm, it seems the currentScene is also here? Why would that be? The GameManager would also be an excellent way to manage the changing of the scenes. More likely done through the game state. Maybe something like:

GameManager.sharedInstance.gameState = GameOverState

Behind the scenes, the gameState setter can then do the magic of swapping out scenes.

These are but a few examples of the practical nature of this approach.

Regarding the talk of loading/storing, this is can also be done through the GameManager if desired. Although for myself, I typically treat it independently based on some data within the GameManager. And this brings up another distinction. I would typically encapsulate ideas within the GameManager. For example, I would probably have a Player and that would make GameManager look like this:

class GameManager : NSObject {
    // Overall game state, GameState is probably some enum
    var state:GameState
    var lastState:GameState

    // Sometimes tracking scenes is useful
    var currentScene:SKScene?
    var nextScene:SKScene? // Sometimes helpful if you need to construct the next scene and it takes a non-trivial amount of time

    // These are the globals we need to share to different "parts" of the game
    // Note player state is done through this object. This way I can pass around only player when needed.
    var player: Player

    var stage: Uint

    var highscores:[Uint]   // A list of all the high score for the user. You would probably want HS based on other users too

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

Wait you say; "But I don't like this global/singleton jazz. Why don't I just pass this object through to everyone that needs it." The answer is "you can". But in practice, you'll start to find this becomes tedious. You also may find that you discover you need the GameManager, but the caller never had the GameManager passed to it. This means now you're reworking a few methods to pass this object through.

Soooo are globals/singletons evil? You can be your own judge for it. Everyone will have there solution of choice. And if it works for you, use it. For me, it's a no brainer. The benefits of using it for consistent ease of use in accessing/managing global game state across your implementation make it an obvious choice.

So I'm adding a little more info here for an already long answer to try clarify a few things.

My choice of singleton or global would be on usage, platform, and language. It is also dependent upon the game code base as well. It is not meant to be a dumping ground of just any data. If utilized, the contents should be carefully considered and when possible, use other containers for the contents (ie. wrap them in classes). So while my first example had player information dangling out (done to more easily convey the idea), in reality I would have PlayerState contain player data. Note this may not be the player in it's entirety, but perhaps some shared info that needs to live beyond the in-game life of the player.

class PlayerState {
    var score:Uint
    var highscore:Uint
    var lives:Uint
}

// Simplified GameManager
class GameManager {
    var state:GameState
    var lastState:GameState

    var player:PlayerState

    // Egads a singleton!
    static let sharedInstance = GameManager()
}

Additionally, I would end up passing around the PlayerState instance versus expecting code to always grab it through the global/singleton.

So for example I would do this:

func doSomething(playerState:PlayerState) {
    var score = playerState.score   

    // Blah blah blah
}

verus

func doSomething() {
    // Whee I have singleton
    var score = GameManager.player.score

    // Blah blah blah
}

Effectively what I'm doing is using the singleton as anchor points to access data objects more easily and then if needed, pass it around. In other words, While they are being employed, I also do not want my code riddled with GameManager.sharedInstance.

As mentioned, there are certainly a bunch of cons to this approach. One of which is concurrency. However, I will point out that concurrency still can occur with data even if it is not in a global/singleton.

My point of this addition is that game dev (and coding in general) involves a degree of practicality. Not only are you doing a balancing act with the system resources to present your game, but presumably you're making the game for people to play it. And for that to happen you need to get it done. Everyone wants to make a perfect snowflake, but what is the timecost for that perfection? At the start of the answer I indicated this technique is a tool. Globals/singletons won't be for everyone. At the same time, one should also not just blindly follow design mantras. If it can be an effective tool for you and help you finish your game faster, then its worthwhile doing. There will always be another game to write and you can then decide based on experience what worked and what did not.

4
votes

During the execution of a game many things could happen, a game have a crash, the player close the app, internet stops, a node could be paused from a parent (so it will be skipped when a scene processes actions). For this reason I agree with Simone Pistecchia comments: you could prepare a "storage manager" class to load and save to file your player stats, use NSCoding to declare and initialize them, use a Shared Instance class to handle them across the project. With this way you could load and save your vars when you need and show them also out of the game (to your main menu for a "High Scores" scene for example of wherever you want)