SpriteKit中优化多关卡秘豆UserDefaults存储方案的技术问询
嘿,这个问题我之前做SpriteKit关卡收集系统的时候也碰到过!随着关卡增加,那种重复定义变量、写一堆else if的方式确实会变得超级臃肿,完全懂你的痛苦。下面是我整理的几个更高效的实现思路,应该能帮你把代码精简不少:
方案一:用Codable结构体统一管理进度
这种方式把所有秘豆收集状态打包成一个对象,一次性存入UserDefaults,不用再为每个关卡单独设置键值对,而且逻辑封装性更强。
步骤1:定义进度结构体
先创建一个遵循Codable的结构体,封装所有收集状态和操作方法:
struct SecretBeanProgress: Codable { // 用字典存储「关卡号: 是否收集」的映射,支持非连续关卡号 private var collectedLevels: [Int: Bool] = [:] // 自动计算已收集数量,不用手动维护总数(避免和实际状态不一致) var collectedCount: Int { collectedLevels.values.filter { $0 }.count } // 检查指定关卡是否已收集秘豆 func hasCollectedBean(for level: Int) -> Bool { collectedLevels[level] ?? false } // 标记指定关卡的秘豆为已收集 mutating func markBeanCollected(for level: Int) { collectedLevels[level] = true } // 重置所有收集进度 mutating func reset() { collectedLevels.removeAll() } }
步骤2:扩展UserDefaults实现存储
给UserDefaults加个扩展,方便读写这个结构体:
extension UserDefaults { private enum StorageKeys: String { case secretBeanProgress } // 自定义属性,直接读写SecretBeanProgress对象 var secretBeanProgress: SecretBeanProgress { get { guard let data = data(forKey: StorageKeys.secretBeanProgress.rawValue) else { return SecretBeanProgress() } do { return try JSONDecoder().decode(SecretBeanProgress.self, from: data) } catch { print("解码秘豆进度失败: \(error)") return SecretBeanProgress() } } set { do { let data = try JSONEncoder().encode(newValue) set(data, forKey: StorageKeys.secretBeanProgress.rawValue) } catch { print("编码秘豆进度失败: \(error)") } } } }
步骤3:在游戏场景中使用
现在不管哪个关卡,代码都变得异常简洁:
- 加载关卡时移除已收集的秘豆:
// 假设当前关卡号是levelNumber(比如1、2...,建议抽成常量或配置) let progress = UserDefaults.standard.secretBeanProgress if progress.hasCollectedBean(for: levelNumber) { // 用可选绑定避免强制解包崩溃 if let secretBean = childNode(withName: "SecretBean") as? SKSpriteNode { secretBean.removeFromParent() } } - 收集秘豆时更新状态:
if node.name == "SecretBean" { var progress = UserDefaults.standard.secretBeanProgress progress.markBeanCollected(for: levelNumber) UserDefaults.standard.secretBeanProgress = progress // 检查是否收集全所有秘豆(假设totalLevels是总关卡数) if progress.collectedCount == totalLevels { // 触发全收集奖励逻辑 } } - 重置新游戏时清空进度:
var progress = UserDefaults.standard.secretBeanProgress progress.reset() UserDefaults.standard.secretBeanProgress = progress
方案二:用数组简化存储(适合连续关卡)
如果你的关卡是连续递增的(1、2、3...),用数组会更简单,数组索引+1对应关卡号:
extension UserDefaults { private enum StorageKeys: String { case collectedBeanLevels } private var collectedBeanLevels: [Bool] { get { array(forKey: StorageKeys.collectedBeanLevels.rawValue) as? [Bool] ?? [] } set { set(newValue, forKey: StorageKeys.collectedBeanLevels.rawValue) } } func hasCollectedBean(for level: Int) -> Bool { guard level > 0, level <= collectedBeanLevels.count else { return false } return collectedBeanLevels[level - 1] } func markBeanCollected(for level: Int) { var levels = collectedBeanLevels // 确保数组长度足够容纳当前关卡 while levels.count < level { levels.append(false) } levels[level - 1] = true collectedBeanLevels = levels } var collectedBeanCount: Int { collectedBeanLevels.filter { $0 }.count } func resetBeanProgress() { collectedBeanLevels = [] } }
使用方式和结构体方案类似,调用对应的方法即可,代码同样简洁。
额外优化建议
- 把总关卡数
totalLevels抽成全局常量或配置文件,避免硬编码; - 可以封装一个
GameProgressManager单例类,把所有进度相关的逻辑都放在里面,各个场景直接调用单例方法,代码更模块化; - 避免强制解包
childNode(withName:),用可选绑定更安全,防止节点不存在时崩溃。
这样改完之后,不管你后续加多少关卡,都只需要在对应的地方传入关卡号就行,再也不用复制粘贴那些重复的UserDefaults代码了!
内容的提问来源于stack exchange,提问作者genericguy25
相关产品推荐
相关产品推荐

