Subclass of Skspritenode in .Sks File

Subclass of SKSpriteNode in .sks file

There are a couple of issues here to address...

How .sks loading works

When you load a .sks file, SpriteKit instantiates everything within using NSKeyedUnarchiver. This means that all the nodes inside are loaded as whatever base classes they were specified as by Xcode when it created the .sks file — SKSpriteNode for sprites with texture art, SKLabelNode for text, SKFieldNode for physics fields, etc. And Xcode doesn't currently provide an option for setting custom classes for the nodes inside a .sks file.

(The one exception for this is changing the runtime class of the scene itself — the top level container for everything in the .sks file. And that's only because of the custom SKNode.unarchiveFromFile implementation provided for you in the project template. Its technique for changing classes at load time works when you have one and only one instance of a particular class in an archive — good for SKScene, not so good for the many nodes in a scene.)

How casting works

When you write something like:

myObj = childNodeWithName("block1") as Block

You're telling the compiler something like: "Hey, you know that thing you got from childNodeWithName? All you know is that it's an SKNode, but I know it's really a Block, so please let me call Block methods on it." (And the compiler says, "Okay, whatever.")

But then at run time, that thing you got had better really be a Block, or your app will crash because you tried to do something Blocky with something that's not a Block. And, per the bit about .sks loading above, that thing isn't and can't be a Block — Xcode doesn't know how to put Blocks into a .sks file. So you can't get a Block out of it, so your app is guaranteed to crash.

Workarounds

So, if you can't put custom classes into a .sks file, what can you do? It depends a bit on what exactly you're trying to accomplish. But there's a good trick that might also be good game/app design in general: use the .sks file for general layout and configuration, and use a second pass to bring in things that need custom behavior.

For example, if you're building a level in a 2D platform game, you wouldn't really want to have the .sks file contain an instance of your Plumber class even if you could — that class probably has lots of details about how tall or fat the guy is, how high he jumps, the shape of his mustache, etc, and you don't want to have to set those up again every time you make a new level, much less have them saved again in each level's .sks file. Instead, the only thing you really need to know in each level file is the position he starts at. So, drag out an "Empty Node" in Xcode, and at load time, replace that node with an instance of your Plumber class, like so:

let spawnPoint = childNodeWithName("spawnPoint")
let player = Plumber()
player.position = spawnPoint.position
addChild(player)
spawnPoint.removeFromParent()

If you have more configuration details that you want to set in the .sks file, you might consider automating that process.

  1. Make a method that does the above node-swapping trick. (Call it something like replaceNode(_:withNode:).)
  2. Make an initializer for your custom class that takes a SKNode or SKSpriteNode, and have it set all its inherited properties (or at least the ones you care about, like color and texture) from that node.
  3. Use enumerateChildNodesWithName:usingBlock: with a search pattern to find all the nodes in your scene with a certain kind of name, and replace them with a new node created using your initializer. Something like:

    enumerateChildNodesWithName("//brick_[0-9]*") { node, stop in
    self.replaceNode(node, withNode: BrickBlock(node))
    }
    enumerateChildNodesWithName("//question_[0-9]*") { node, stop in
    self.replaceNode(node, withNode: QuestionBlock(node))
    }

custom SKSpriteNode classes for use in .sks file?

no it's not possible to use @IBInspectable, that only works with classes in the storyboard editor.

you can create your custom class with an init and instantiate it from code. If you want to instatiate your custom object from the Scene editor you MUST use the init(coder:) func

you can have both init's in your class in case you wish to instantiate your object in code at some point as well as creating in a scene sks file.

init() {

super.init(texture: nil, color: .clear, size, CGSize.zero)

setup()
}

required init?(code aDecoder: NSCoder) {

super.init(code: aDecoder)

setup()
}

func setup() {
//add some setup code here
}

Or if you ONLY want to instantiate the object in the scene file you can eliminate the normal init().

Cannot create subclass of SKSpriteNode

Take notice to the state of your scene file. If you see it shaded, this means it has not saved yet. If you compile your code while still in the scene editor, the save will not happen, so be sure to hit cmd + s to save prior to compiling.
Sample Image

Now in case people are trying to figure out why class names are not saving, make sure you hit ENTER or leave the text field and go to another textfield to ensure that the class name saves, otherwise it will revert back to an older state.

Sample Image

Subclassing SKNodes created with SpriteKit .sks scene file

I wrote a little helper delegate to deal with this: https://github.com/ice3-software/node-archive-delegate. It uses the NSKeyedUnarchiverDelegate to subclass your sprites by name.

Load Spritekit scene from a subclass

It's not necessary as in .sks there is a custom class. You just put "Spielfeld" inside.

Then in viewController:

if let sceneNode = SKScene.init(fileNamed: "Spielfeld(Whatever your name is here)") { 
(sceneNode as! Spielfeld).property = "anyValueToUse"

if let view = self.view as! SKView? {
view.presentScene(sceneNode) //present the scene.
}

If you decided to load from a file, there is no reason to init again. You may add init functions by overriding the following:

   override func sceneDidLoad() {  
} //called after .sks is loaded.

override func didMove(to view: SKView) {
//change the size here.
} // called after presentation.

Subclassing with custom initializer from .sks file

Associating your Custom Class to a Node

If you want to associate a node you create into your SKS file to a custom class, you need to:

  1. Add an empty node into your SKS file
  2. Select the Node
  3. Open the Custom Class inspector in Xcode
  4. Type the name of your class into the Custom Class field
  5. Type the name of your project into the Module field

Sample Image

Using the right initializer

When SpriteKit does load the SKS file and start building the objects to populate the scene it does not call you custom initializer but this one

required init?(coder aDecoder: NSCoder)

so your Enemy class should be defined like this

class Enemy: SKSpriteNode {
let health: Int

required init?(coder aDecoder: NSCoder) {
self.health = 10

let texture = SKTexture(imageNamed: "Spaceship")
super.init(texture: texture, color: .clearColor(), size: texture.size())
}
}

Test

You can now test that you have a real Enemy object into your scene defining this method into your GameScene

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let enemy = (children.filter { $0 is Enemy }).first else { fatalError("No Enemy found") }
print(enemy)
}

Using your custom initializer

Right now there is no way of passing parameter from the SKS file to your custom initializer.

However such a technique was available into CocosBuilder, an old Game Level editor available for Cocos2d so I believe sometime in the future we will be able to pass parameters from the SKS file to our custom classes.

Maybe this will be announced with the next version of SpriteKit in a few days during the WWDC 2016.

Using an .sks file to layout a complex object vs a SKScene file

You can use this delegate https://github.com/ice3-software/node-archive-delegate

Or, smt like this in swift:

class func unarchiveNodeFromFile(file:String)-> SKNode? {
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") {
var fileData = NSData.dataWithContentsOfFile(path, options: NSDataReadingOptions.DataReadingMappedIfSafe, error: nil)
var archiver = NSKeyedUnarchiver(forReadingWithData: fileData)
archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKNode")
let node = archiver.decodeObjectForKey(NSKeyedArchiveRootObjectKey) as SKNode
archiver.finishDecoding()
return node
} else {
return nil
}
}

UPDATE for Swift 3 and fix class casting:

func unarchiveNodeFromFile(file:String)-> SKNode? {
if let path = Bundle.main.path(forResource: file, ofType: "sks") {
let fileData = NSData.init(contentsOfFile: path)
let archiver = NSKeyedUnarchiver.init(forReadingWith: fileData as Data!)
archiver.setClass(SKNode.classForKeyedUnarchiver(), forClassName: "SKScene")
let node = archiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as! SKNode
archiver.finishDecoding()
return node
} else {
return nil
}
}

using sks file with SKScene subclass

Example of connection of your scene files with .sks file is given in the default SK game template. GameViewController has a category "SKScene (Unarchive)" for this.

+ (instancetype)unarchiveFromFile:(NSString *)file {
/* Retrieve scene file path from the application bundle */
NSString *nodePath = [[NSBundle mainBundle] pathForResource:file ofType:@"sks"];
/* Unarchive the file to an SKScene object */
NSData *data = [NSData dataWithContentsOfFile:nodePath
options:NSDataReadingMappedIfSafe
error:nil];
NSKeyedUnarchiver *arch = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[arch setClass:self forClassName:@"SKScene"];
SKScene *scene = [arch decodeObjectForKey:NSKeyedArchiveRootObjectKey];
[arch finishDecoding];

return scene;
}

You can implement the same category in your own class.

Then you just call:

MyScene *scene = [MyScene unarchiveFromFile:@"MyScene"];


Related Topics



Leave a reply



Submit