5

I have build a swift game with a "GameViewController.swift"

import UIKit
import SpriteKit

class GameViewController: UIViewController {


override func viewDidLoad() {
    super.viewDidLoad()


    if let scene = GameScene(fileNamed:"GameScene") {
        // Configure the view.
        let skView = self.view as! SKView

        /* Set the scale mode to scale to fit the window */


        scene.scaleMode = .AspectFill

        skView.presentScene(scene)
        }
    }
}

and i have a scene called "GameScene"(it is the default game-file when you open swift and create a new swift game)

import UIKit
import SpriteKit

class GameScene: SKScene{

var building = SKSpriteNode()

override func didMoveToView(view: SKView) {

    /* Setup your scene here */

}

a lot of things going on there. (buildings can be placed and that trigger some actions and so on)
But all the progress is lost when you leave the app.
How to save the whole progress of this "GameScene" and reload it at the start?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
frankibanki
  • 115
  • 6

3 Answers3

5

You can archive your game state in the applicationDidEnterBackground(_:) event. Subscribe through NotificationCenter to get notified.

class GameScene : SKScene {
    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)

        NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground), name: .UIApplicationDidEnterBackground, object: nil)
    }

    func applicationDidEnterBackground() {
        // Save Scene
        let sceneData = NSKeyedArchiver.archivedData(withRootObject: self)
        UserDefaults.standard.set(sceneData, forKey: "currentScene")
    }

    class func loadScene() -> SKScene? {
        var scene: GameScene?

        if let savedSceneData = UserDefaults.standard.object(forKey: "currentScene") as? NSData,
           let savedScene = NSKeyedUnarchiver.unarchiveObject(with: savedSceneData as Data) as? GameScene {
            scene = savedScene
        } else {
            scene = nil
        }

        return scene
    }
}

Scene loading happens in viewDidLoad, by first attempting to locate a previously archived scene or creating a new scene if no archived scene exists.

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        if let view = self.view as! SKView? {
            guard let scene = GameScene.loadScene() ?? SKScene(fileNamed: "GameScene") as? GameScene else {
                fatalError("Scene not loaded")
            }

            scene.scaleMode = .resizeFill

            view.presentScene(scene)            
            view.ignoresSiblingOrder = true
        }
    }
}

You'll also need to make game elements NSCoding compliant, as mentioned in other answers.

Mark Brownsword
  • 2,287
  • 2
  • 14
  • 23
4

SKScene conforms to NSCoding, so you can use this to archive your scene. Here is a tutorial that may help. https://www.hackingwithswift.com/read/12/3/fixing-project-10-nscoding

The basic idea is you take the instance of your scene, and you archive it with:

NSKeyedArchiver.archivedData(withRootObject: scene)

Then take this and save it to disk or somewhere else

You can then unarchive it later with

NSKeyedUnarchiver.unarchiveObject(with: scene) as! SKScene

Now when you make a custom scene class, you need to be sure to include any variables you create that you need to save. This is where that pesky required init(coder aDecoder:NSCoder) initializer comes in that people tend to not use.

You basically use this initializer to decode your custom variables, like this.

required init(coder aDecoder: NSCoder) {
    player = aDecoder.decodeObject(forKey: "player") as! String
}

To save it to the archive, you add an encode method

func encode(with aCoder: NSCoder) {
    aCoder.encode(player, forKey: "player")
}
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
0

Thanks to Mark Brownsword's 2017 answer! In 2023, the solution is now:

private let kWorldScene = "kWorldScene"

class WorldScene: SKScene {

    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)

        NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActiveNotification), name: UIApplication.willResignActiveNotification, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc func applicationWillResignActiveNotification() {
        if let sceneData = try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) {
            NSLog("archive \(sceneData.count) bytes")
            UserDefaults.standard.set(sceneData, forKey: kWorldScene)
        }
    }

    static func loadScene() -> WorldScene? {
        do {
            guard let sceneData = UserDefaults.standard.object(forKey: kWorldScene) as? Data else { return nil }
            NSLog("unarchive \(sceneData.count) bytes")
            return try NSKeyedUnarchiver.unarchivedObject(ofClass: WorldScene.self, from: sceneData)
        } catch {
            NSLog("unarchive error: \(error)")
            return nil
        }
    }

    /// Class '...' has a superclass that supports secure coding, but '...' overrides -initWithCoder: and does not override +supportsSecureCoding.
    /// The class must implement +supportsSecureCoding and return YES to verify that its implementation of -initWithCoder: is secure coding compliant.
    static override var supportsSecureCoding: Bool {
        return true
    }

    ...
}

Be sure to update SKNode subclass init(coder:) and supportsSecureCoding: as well.

As before, the ViewController initializes the scene:

class WorldViewController: UIViewController {

    ...

    @IBOutlet private var worldView: WorldView!

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        let scene = WorldScene.loadScene() ?? WorldScene(size: self.worldView.frame.size)
        scene.scaleMode = .resizeFill
        self.worldView.presentScene(scene)
        self.worldView.ignoresSiblingOrder = true
    }

    ...
Mark
  • 681
  • 7
  • 15