You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpriteKit自定义类含SKSpriteNode时如何使用Codable协议?

解决自定义Codable类包含SKSpriteNode属性的问题

这个问题很典型——SpriteKit的节点类(比如SKSpriteNode)并没有实现Codable协议,所以当你的自定义类包含这类属性时,Swift无法自动生成Decodable/Encodable的实现,就会抛出你看到的错误。

解决的核心思路是:不要直接编码SKSpriteNode实例,而是编码重建它所需要的关键信息,解码时再用这些信息重新创建节点。下面给你两种实用的方案,根据你的需求选择:


方案1:保存SpriteNode的动态状态(适合节点状态会变化的场景)

如果你的dark和light节点可能在运行时改变位置、隐藏状态或其他属性,需要完整保存这些状态,那么可以手动实现Codable的编码和解码逻辑,提取并存储节点的关键可序列化属性。

修改后的TileNode代码示例:

class TileNode: SKNode, Codable {
    // MARK: - Class Variables
    var height = 32
    var width = 32
    var index = 0
    var row = 0
    var column = 0
    var tileType = ""
    
    // SpriteNode属性(无法自动编码)
    var dark = SKSpriteNode()
    var light = SKSpriteNode()
    
    // MARK: - Init
    override init() {
        super.init()
    }
    
    init(index:Int, tex: String) {
        super.init()
        self.isUserInteractionEnabled = false
        self.index = index
        self.zPosition = 10
        self.name = "Tile \(index)"
        self.tileType = tex
        
        // 初始化dark和light节点(示例逻辑,可根据你的实际代码调整)
        dark = SKSpriteNode(imageNamed: "\(tex)_dark")
        dark.position = CGPoint(x: width/2, y: height/2)
        dark.zPosition = 1
        addChild(dark)
        
        light = SKSpriteNode(imageNamed: "\(tex)_light")
        light.position = CGPoint(x: width/2, y: height/2)
        light.zPosition = 2
        light.isHidden = true
        addChild(light)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - 手动实现Codable
    enum CodingKeys: String, CodingKey {
        // 基本属性
        case height, width, index, row, column, tileType
        // dark节点的关键状态
        case darkTextureName, darkPosX, darkPosY, darkZPos, darkIsHidden
        // light节点的关键状态
        case lightTextureName, lightPosX, lightPosY, lightZPos, lightIsHidden
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        // 编码基本属性
        try container.encode(height, forKey: .height)
        try container.encode(width, forKey: .width)
        try container.encode(index, forKey: .index)
        try container.encode(row, forKey: .row)
        try container.encode(column, forKey: .column)
        try container.encode(tileType, forKey: .tileType)
        
        // 编码dark节点的状态
        if let textureName = dark.texture?.imageName {
            try container.encode(textureName, forKey: .darkTextureName)
        }
        try container.encode(dark.position.x, forKey: .darkPosX)
        try container.encode(dark.position.y, forKey: .darkPosY)
        try container.encode(dark.zPosition, forKey: .darkZPos)
        try container.encode(dark.isHidden, forKey: .darkIsHidden)
        
        // 编码light节点的状态
        if let textureName = light.texture?.imageName {
            try container.encode(textureName, forKey: .lightTextureName)
        }
        try container.encode(light.position.x, forKey: .lightPosX)
        try container.encode(light.position.y, forKey: .lightPosY)
        try container.encode(light.zPosition, forKey: .lightZPos)
        try container.encode(light.isHidden, forKey: .lightIsHidden)
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // 解码基本属性
        height = try container.decode(Int.self, forKey: .height)
        width = try container.decode(Int.self, forKey: .width)
        index = try container.decode(Int.self, forKey: .index)
        row = try container.decode(Int.self, forKey: .row)
        column = try container.decode(Int.self, forKey: .column)
        tileType = try container.decode(String.self, forKey: .tileType)
        
        // 初始化父类
        super.init()
        
        // 重建dark节点
        let darkTexName = try container.decode(String.self, forKey: .darkTextureName)
        dark = SKSpriteNode(imageNamed: darkTexName)
        dark.position = CGPoint(
            x: try container.decode(CGFloat.self, forKey: .darkPosX),
            y: try container.decode(CGFloat.self, forKey: .darkPosY)
        )
        dark.zPosition = try container.decode(CGFloat.self, forKey: .darkZPos)
        dark.isHidden = try container.decode(Bool.self, forKey: .darkIsHidden)
        addChild(dark)
        
        // 重建light节点
        let lightTexName = try container.decode(String.self, forKey: .lightTextureName)
        light = SKSpriteNode(imageNamed: lightTexName)
        light.position = CGPoint(
            x: try container.decode(CGFloat.self, forKey: .lightPosX),
            y: try container.decode(CGFloat.self, forKey: .lightPosY)
        )
        light.zPosition = try container.decode(CGFloat.self, forKey: .lightZPos)
        light.isHidden = try container.decode(Bool.self, forKey: .lightIsHidden)
        addChild(light)
        
        // 恢复TileNode自身的基础配置
        self.isUserInteractionEnabled = false
        self.zPosition = 10
        self.name = "Tile \(index)"
    }
}

方案2:仅保存生成SpriteNode的源数据(适合节点状态固定的场景)

如果dark和light节点的状态完全由tileType等基础属性决定,不会在运行时改变,那么可以简化逻辑:只编码基础属性,解码时通过初始化方法重新创建节点。

修改后的TileNode代码示例:

class TileNode: SKNode, Codable {
    // MARK: - Class Variables
    var height = 32
    var width = 32
    var index = 0
    var row = 0
    var column = 0
    var tileType = ""
    
    // SpriteNode属性(无需编码)
    var dark = SKSpriteNode()
    var light = SKSpriteNode()
    
    // MARK: - Init
    override init() {
        super.init()
        setupTileNodes()
    }
    
    init(index:Int, tex: String) {
        super.init()
        self.isUserInteractionEnabled = false
        self.index = index
        self.zPosition = 10
        self.name = "Tile \(index)"
        self.tileType = tex
        
        setupTileNodes()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 提取节点创建逻辑,方便复用
    private func setupTileNodes() {
        dark = SKSpriteNode(imageNamed: "\(tileType)_dark")
        dark.position = CGPoint(x: width/2, y: height/2)
        dark.zPosition = 1
        addChild(dark)
        
        light = SKSpriteNode(imageNamed: "\(tileType)_light")
        light.position = CGPoint(x: width/2, y: height/2)
        light.zPosition = 2
        light.isHidden = true
        addChild(light)
    }
    
    // MARK: - Codable实现
    enum CodingKeys: String, CodingKey {
        // 只编码基础属性,排除SpriteNode
        case height, width, index, row, column, tileType
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // 解码基础属性
        height = try container.decode(Int.self, forKey: .height)
        width = try container.decode(Int.self, forKey: .width)
        index = try container.decode(Int.self, forKey: .index)
        row = try container.decode(Int.self, forKey: .row)
        column = try container.decode(Int.self, forKey: .column)
        tileType = try container.decode(String.self, forKey: .tileType)
        
        super.init()
        
        // 调用复用逻辑创建节点
        self.isUserInteractionEnabled = false
        self.zPosition = 10
        self.name = "Tile \(index)"
        setupTileNodes()
    }
    
    // encode方法可自动合成,也可手动实现(效果一致)
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(height, forKey: .height)
        try container.encode(width, forKey: .width)
        try container.encode(index, forKey: .index)
        try container.encode(row, forKey: .row)
        try container.encode(column, forKey: .column)
        try container.encode(tileType, forKey: .tileType)
    }
}

关键注意事项

  • 永远不要尝试直接编码SKNode或其子类实例:这类对象包含大量渲染相关的内部状态和循环引用,无法被序列化。
  • 编码的原则是只保存重建对象所需的最小数据:避免存储冗余信息,既减少存储体积,也降低解码出错的概率。

内容的提问来源于stack exchange,提问作者zeeple

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 09:09:07