Search code examples
iosswiftsprite-kit

SpriteKit unable to unarchive an SKEmitterNode subclass from sks file


I have the following code:


final class PassthroughEmitterNode: SKEmitterNode {
  
  static func load(fnWithoutExtension: String, in bundle: Bundle) -> SKEmitterNode? {
    guard
      let sksPath = bundle.path(forResource: fnWithoutExtension, ofType: "sks"),
      let sksData = try? Data(contentsOf: URL(fileURLWithPath: sksPath)),
      let unarchiver = try? NSKeyedUnarchiver(forReadingFrom: sksData),
      let texturePath = bundle.path(forResource: fnWithoutExtension, ofType: "png"),
      let textureImage = UIImage(contentsOfFile: texturePath)
    else { return nil }
    
    // Required to decode into subclass
    unarchiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKEmitterNode")
    let emitter = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? PassthroughEmitterNode
    unarchiver.finishDecoding()
    
    guard let emitter else { return nil }
    
    // We still need to set texture, because the texture file is not in main bundle
    emitter.particleTexture = SKTexture(image: textureImage)
    
    // Have to enable user interaction to receive touch
    emitter.isUserInteractionEnabled = true
    
    return emitter
  }
}

This is very similar to reading a SKScene subclass instance from SKS file (such as this: using sks file with SKScene subclass).

However, this doesn't work. The unarchiver's decoding always returns nil.

But unarchiving to SKEmitterNode class itself works. The subclass doesn't work.


Solution

  • We can try this strategy:

    1. Decode SKEmitterNode
    2. Create a PassthroughEmitterNode from a SKEmitterNode

    In order to do that let's add this init

    public final class PassthroughEmitterNode: SKEmitterNode {
        
        init(emitterNode: SKEmitterNode) {
            super.init()
    
            self.emissionAngle = emitterNode.emissionAngle
            self.emissionAngleRange = emitterNode.emissionAngleRange
            self.numParticlesToEmit = emitterNode.numParticlesToEmit
            self.particleAlpha = emitterNode.particleAlpha
            self.particleAlphaRange = emitterNode.particleAlphaRange
            self.particleAlphaSpeed = emitterNode.particleAlphaSpeed
            self.particleBirthRate = emitterNode.particleBirthRate
            self.particleBlendMode = emitterNode.particleBlendMode
            self.particleColor = emitterNode.particleColor
            self.particleColorBlendFactor = emitterNode.particleColorBlendFactor
            self.particleColorBlendFactorRange = emitterNode.particleColorBlendFactorRange
            self.particleColorBlendFactorSpeed = emitterNode.particleColorBlendFactorSpeed
            self.particleLifetime = emitterNode.particleLifetime
            self.particleLifetimeRange = emitterNode.particleLifetimeRange
            self.particlePositionRange = emitterNode.particlePositionRange
            self.particleRotation = emitterNode.particleRotation
            self.particleRotationRange = emitterNode.particleRotationRange
            self.particleRotationSpeed = emitterNode.particleRotationSpeed
            self.particleScale = emitterNode.particleScale
            self.particleScaleRange = emitterNode.particleScaleRange
            self.particleScaleSpeed = emitterNode.particleScaleSpeed
            self.particleSpeed = emitterNode.particleSpeed
            self.particleSpeedRange = emitterNode.particleSpeedRange
            self.particleTexture = emitterNode.particleTexture
            self.position = emitterNode.position
            self.xAcceleration = emitterNode.xAcceleration
            self.yAcceleration = emitterNode.yAcceleration
            // TODO: Add additional properties I might have forgot.
        }
        
        @available(*, unavailable)
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
    
    

    That's it.

    Now you can decode a standard SKEmitterNode and convert it into your PassthroughEmitterNode

    let emitterNode: SKEmitterNode = ... // Decode as usual
    let passthroughEmitterNode = PassthroughEmitterNode(emitterNode: emitterNode)
    

    What if PassthroughEmitterNode has additional properties compared to SKEmitterNode?

    The value for these additional properties cannot come from the sks file since there is no place for them. So you will need to populate them using some default value or custom logic.