简体   繁体   中英

Store multiple userdefaults variables in some kind of array, class, or struct in spritekit?

Right now the gameplay features multiple levels and there is a secret item (a bean) in every level that the player will only be able to find once per level, per save file. The idea is that if the player can find each secret bean per level, once they get to the end of the game they get a bonus. I track when a player has or hasn't found the secret item in each level via user defaults, it works but my implementation is ugly and I need help with it.

The issue is I can't wrap my head around an efficient way to do this per level. In order to track all of this per level I've been doing this:

//Class gameScene variable this tracks per level if the player found the bean, if so then it's set to true and the bean will not load in the level if the player revisits

 var foundBeanLevel1 = UserDefaults().bool(forKey: "FoundBeanLevel1")
 var foundBeanLevel2 = UserDefaults().bool(forKey: "FoundBeanLevel2")

this is in each levels override didMove (replace 1 with X level):

 if foundBeanLevel1{
    let secretBean: SKSpriteNode = childNode(withName: "SecretBean") as! SKSpriteNode
    secretBean.removeFromParent()
 }

_

//total count, if it's not the sum of the number of levels then the player didn't collect all beans
var secretBeanCount: Int = UserDefaults().integer(forKey: "SavedSecretBean") {
    didSet {
        //sets the increment to user defaults
        UserDefaults().set(secretBeanCount, forKey: "SavedSecretBean")
    }
}

//didContact
if node.name == SecretBean{
    secretBeanCount += 1
    if levelCheck == 1 {
        UserDefaults().set(true, forKey: "FoundBeanLevel1")
    } else if levelCheck == 2 {
        UserDefaults().set(true, forKey: "FoundBeanLevel2")
    }
}

and then if they start a new game:

UserDefaults().set(0, forKey: "SavedSecretBean")
UserDefaults().set(false, forKey: "FoundBeanLevel1")
UserDefaults().set(false, forKey: "FoundBeanLevel2")

This system works as intended but I know it's very sloppy. Also the more levels I get the larger these else if statements will become. I know there is a much better way to do this, but I'm not sure what I'm looking for in that regard. Any advice would be greatly appreciated.

You could consider storing bean level numbers in a Set :

var beanLevels = Set<Int>()
beanLevels.insert(1)
beanLevels.insert(3)
beanLevels.insert(1)

A set makes sure an element is stored only once so in the above example the set will only contain 1 and 3.

Membership test is super-easy with a set:

if beanLevels.contains(1) {
    // ...
}

Also, you don't need the SavedSecretBean any longer. Instead you can test if the set contains any bean by this:

if beanLevels.isEmpty {
    // ...
}

By converting the set into an array you can easily save it to UserDefaults and restore it from there:

// Save
let beanLevelsArray = Array(beanLevels)
UserDefaults.standard.set(beanLevelsArray, forKey: "Bean Levels")

// Restore
if let beanLevelsArray = UserDefaults.standard.array(forKey: "beanLevels") as? [Int] {
    let beanLevels = Set(beanLevelsArray)
}

This way you get rid of the necessity to statically configure variables depending on the number of levels in your game.

Expanding on Tom E's smart suggestion of using Sets and saving to UserDefaults, you might also want to save all the other GameVariables in one object and use it easily. If you insist on using UserDefaults I suggest using Codable and this is what the implementation might look like:

struct GameSceneVariables: Codable {
    let beanLevels: Set<Int>
    let savedSecretBean: Int
}

extension UserDefaults {

    /// Saves a Codable Object into UserDefaults
    ///
    /// - Parameters:
    ///   - object: The object to save that conforms to Codable
    ///   - key: The key to save and fetch
    /// - Returns: if the save was successful or failed
    func set<T:Codable>(_ object: T, key: String) -> Bool {
        guard let encodedValue = try? JSONEncoder().encode(object) else {
            return false
        }
        UserDefaults.standard.set(encodedValue, forKey: key)
        return true
    }

    /// Retrieves a Codable object saved in UserDefaults
    ///
    /// - Parameters:
    ///   - key: The key that was used to save the object into UserDefaults
    ///   - type: The type of the object so that you would not to actually cast the object so that the compiler
    ///           can know what Type of object to Return for the generic parameter T
    /// - Returns: returns the object if found, or nil if not found or if not able to decode the object
    func object<T:Codable>(forKey key: String, forObjectType type: T.Type) -> T? {
        guard let data = UserDefaults.standard.value(forKey: key) as? Data else { return nil }
        return try? JSONDecoder().decode(T.self, from: data)
    }

}

let variables = GameSceneVariables(beanLevels: [0, 1, 2], savedSecretBean: 0)
let userDefaults = UserDefaults.standard
let success = userDefaults.set(variables, key: "GameSceneVariables")
if success {
    let fetchedVariables = userDefaults.object(forKey: "GameSceneVariables", forObjectType: GameSceneVariables.self)
} else {
    // This can happen because the properties might not have been encoded properly
    // You will need to read up more on Codable to understand what might go wrong
    print("Saving was not a success")
}

I added comments on what the code does. I highly recommend before using this code to read more about Codable from the Apple Documentation.

User Defaults is designed to hold custom user preferences. When needing to save small amounts of data, it is fine to store it here, but if you plan on using it for larger sets of data, I would recommend you look elsewhere. This is because User Defaults acts as a database, and its values are cached so that you are not reading and writing to the database a lot of times. Having large amounts of data may invalidate the cache, causing the system to fetch from the database more often than necessary. I would recommend just creating a special struct that you use for reading and writing, and have it save to the documents directory. (Remember to turn off access to the document directory from external apps so that people cannot manipulate the save file. I think apple does this by default now)

Example of saving data to disk without resorting to user defaults:

struct MyGameData : Codable{
    enum CodingKeys: String,CodingKey{
        case beans = "beans"
    }

    var beans = [Int:Any]() //Int will reference our level, and AnyObject could be something else, like another dictionary or array if you want multiple beans per level
    static var beans : [Int:Any]{
        get{
            return instance.beans
        }
        set{
            instance.beans = newValue
        }
    }
    private static var instance = getData()
    private static var documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    private static func getData() -> MyGameData{
        let url = documentDirectory.appendingPathComponent("saveData.json", isDirectory: false)

        if !FileManager.default.fileExists(atPath: url.path) {
            return MyGameData()
        }

        if let data = FileManager.default.contents(atPath: url.path) {
            let decoder = JSONDecoder()
            do {
                let gameData = try decoder.decode(MyGameData.self, from: data)
                return gameData
            } catch {
                fatalError(error.localizedDescription)
            }
        } else {
            fatalError("No data at \(url.path)!")
        }
    }




    public static func save(){
        let url = documentDirectory.appendingPathComponent("saveData.json", isDirectory: false)

        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(instance)
            if FileManager.default.fileExists(atPath: url.path) {
                try FileManager.default.removeItem(at: url)
            }
            let _ = FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    func encode(to encoder: Encoder) throws
    {
        do {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(beans, forKey: .beans)
        } catch {
                fatalError(error.localizedDescription)
        }
    }

    init(from decoder: Decoder)
    {
        do {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            beans = try values.decode([Int:Any].self, forKey: .beans)
        } catch {
                fatalError(error.localizedDescription)
        }

    }
}

To use this is real simple:

MyGameData acts as a singleton, so it will be init the first time you go to use it

let beans = MyGameData.beans

Then when you need to save back to the file, just call

MyGameData.save()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM