【问题标题】:Store multiple userdefaults variables in some kind of array, class, or struct in spritekit?在 spritekit 中的某种数组、类或结构中存储多个 userdefaults 变量?
【发布时间】:2018-05-29 14:27:26
【问题描述】:

现在的游戏玩法有多个关卡,每个关卡都有一个秘密物品(一颗豆子),玩家在每个关卡的每个存档文件中只能找到一次。这个想法是,如果玩家可以在每个关卡中找到每个秘密豆子,一旦他们完成游戏,他们就会获得奖励。我通过用户默认值跟踪玩家何时在每个关卡中找到或没有找到秘密项目,它可以工作,但我的实现很丑陋,我需要帮助。

问题是我无法找到一种有效的方法来在每个级别执行此操作。为了跟踪每个级别的所有这些,我一直在这样做:

//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")

这是在每个级别覆盖 didMove(将 1 替换为 X 级别):

 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")
    }
}

然后如果他们开始新游戏:

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

这个系统按预期工作,但我知道它非常草率。此外,我获得的级别越多,这些 else if 语句就会变得越大。我知道有更好的方法可以做到这一点,但我不确定在这方面我在寻找什么。任何建议将不胜感激。

【问题讨论】:

  • UserDefaults 并不是真正设计为您的“保存”文件。我建议您学习如何使用编码协议进行保存,这样您可以保存到磁盘,而不必担心 UserDefaults 的性能问题,因为您超出了为其分配的缓存大小。

标签: ios swift sprite-kit nsuserdefaults userdefaults


【解决方案1】:

扩展 Tom E 使用 Sets 并保存到 UserDefaults 的明智建议,您可能还希望将所有其他 GameVariables 保存在一个对象中并轻松使用它。如果您坚持使用 UserDefaults,我建议您使用 Codable,这就是实现的样子:

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")
}

我在代码的作用上添加了 cmets。我强烈建议在使用此代码之前阅读 Apple 文档中有关 Codable 的更多信息。

【讨论】:

  • 非常感谢,我会研究一下可编码的。我的印象是 userdefaults 就是我所需要的。是什么让 codable 优于 userdefaults?
  • @genericguy25 上面的这段代码仍然使用UserDefaults,但加上CodableCodable 可以轻松地将 encodedecode 对象转换为 Data,它们也可以轻松转换为 json objectsDictionaries。您可能知道,您不能直接将任何对象保存到UserDefaults。上面的代码使用Codable 解决了这个问题
  • 感谢您的解释,我不太明白为什么我应该通过可编码而不是简单的 userdefaults 来做到这一点?
  • @genericguy25 如您所见,Codable 允许您将整个定制的structclass 保存在UserDefaults 中。如果要将结构中的每个属性单独保存在UserDefaults 中,则需要跟踪每个属性的每个keysetget 该属性的值。使用Codable,您可以检索整个structwhole 类,并在检索后直接访问其属性。
  • 啊,我明白了!谢谢你的解释。
【解决方案2】:

您可以考虑将 bean 级别编号存储在 Set 中:

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

一个集合确保一个元素只存储一次,所以在上面的例子中,集合只包含 1 和 3。

成员资格测试非常简单:

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

另外,您不再需要SavedSecretBean。相反,您可以通过以下方式测试该集合是否包含任何 bean:

if beanLevels.isEmpty {
    // ...
}

通过将集合转换为数组,您可以轻松地将其保存到 UserDefaults 并从那里恢复:

// 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)
}

这样您就无需根据游戏中的关卡数量静态配置变量。

【讨论】:

  • 感谢您的回答,非常直接,非常感谢!
【解决方案3】:

用户默认值旨在保存自定义用户首选项。当需要保存少量数据时,将其存储在这里很好,但如果您打算将其用于更大的数据集,我建议您寻找其他地方。这是因为 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)
        }

    }
}

使用它非常简单:

MyGameData 充当单例,所以在你第一次使用时会被初始化

let beans = MyGameData.beans

然后当你需要保存回文件时,只需调用

MyGameData.save()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-22
    • 2011-10-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多