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 Block
y 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 Block
s 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.
- Make a method that does the above node-swapping trick. (Call it something like
replaceNode(_:withNode:)
.) - Make an initializer for your custom class that takes a
SKNode
orSKSpriteNode
, and have it set all its inherited properties (or at least the ones you care about, like color and texture) from that node. 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.
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.
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:
- Add an empty node into your SKS file
- Select the Node
- Open the Custom Class inspector in Xcode
- Type the name of your class into the Custom Class field
- Type the name of your project into the Module field
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
Swift: How to Fully Strip Internal/Inline Symbols
How to Fix Error: This Class Is Not Key Value Coding-Compliant for the Key Tableview.'
Uisearchcontroller in Navigationitem iOS 11 Apple Way
How to Add Caching to Asyncimage
How to Filter Events Created for the Current Date in the Realm Swift
How to Fix ' *Pod* Does Not Support Provisioning Profiles' in Azure Devops Build Agent
How to Reset Hididletime on MACos 10.14
Swiftui: How Do Style Text View with Different Font and Colour on String Subranges
Get the Current Position of Scrollview in Swiftui
Swiftui: How to Draw Filled and Stroked Shape
Uitableviewrowaction with Icon and Text
Scene Kit Performance with Cube Test
Swift, Avaudiorecorder: Error 317: Ca_Debug_String: Inpropertydata == Null
Iterate an Array W/ Explicit Object Type in Swift
Convincing Swift That a Function Will Never Return, Due to a Thrown Exception