How to Use Spritekit Archives with Skspritenode Subclasses

How to use SpriteKit archives with SKSpriteNode subclasses

There's no way to set a custom class for a node in the SpriteKit editor in Xcode 6. (That'd be a great feature request to file with Apple, though.)

However, the sks files you produce in Xcode are just NSKeyedArchiver archives, and you can use NSKeyedUnarchiver options to control how your objects get instantiated at load time. So there's a (limited) option for changing classes at load time — you can see this in the template code when you create a new SpriteKit Game project in Xcode 6.

See the SKNode extension (or category in the ObjC version) in GameViewController.m: it uses the NSKeyedUnarchiver method setClass(_:, forClassName:) to treat the SKScene instance in the archive as an instance of the template project's GameScene class instead. You can extend this pattern to create other custom scene classes from Xcode-created archives.

You'll notice, though, that setClass(_:forClassName:) works on a class-name basis, so it's of limited use if your archive contains several objects of the same class and you want to decode one of them as a different class than the rest. In that case, you might look into using other unarchiver tricks — for example, replacing an object in the unarchiver(_:didDecodeObject:) delegate method.

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.

Spritekit best way to get sprites scene inside of SKSpriteNode subclass

Usually I create addToNode:(SKNode *)parentNode method to handle that:

- (void)addtoNode:(SKNode *)parentNode {
[parentNode addChild:self];
// Do what you need here
}

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))
    }

How to properly animate SKSpriteNode subclasses

Texture Atlas

First of all you need to create a texture atlas into Assets.xcassets. You'll put here the images related to the frames of your character.

Sample Image

Subclassing SKSpriteNode

This is how you create you own sprite with a beginAnimation method

class Croc: SKSpriteNode {

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

func beginAnimation() {
let textureAtlas = SKTextureAtlas(named: "croc")
let frames = ["croc_walk01", "croc_walk02", "croc_walk03", "croc_walk04"].map { textureAtlas.textureNamed($0) }
let animate = SKAction.animateWithTextures(frames, timePerFrame: 0.1)
let forever = SKAction.repeatActionForever(animate)
self.runAction(forever)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

As you can see the beginAnimation method creates a texture atlas using the same name of the texture atlas I previously created in Assets.xcassets.

Then the an array of textures is created (frames) and used as parameter to create the animate action.

Starting the animation

Now you need to create your sprite and invoke beginAnimation just once. You do NOT have to invoke beginAnimations inside any update method otherwise you will create a new animation every new frame. The is wrong. beginAnimation must be called only once.

Here's an example

class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let croc = Croc()
croc.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
addChild(croc)

croc.beginAnimation()
}
}

Sample Image

SpriteKit SKSpriteNode

You should consider using an Array to store the zombie when you add them to the scene. This is faster then enumerating the scene and gives you more flexibility.

// create an array of spriteNodes
var zombieArray:[SKSpriteNode]

//add zombies to array when you add them to scene
zombieArray.append(zombieGreen)

//check if any zombies are in the scene
if zombieArray.count > 0{
.....
}

//Do something with all the zombies in the array - your main question.
for zombie in zombieArray{

.....
zombie.physicsBody?.applyImpulse(vector)
}

// remove zombie from array
zombieArray.remove(at: zombieArray.index(of: theZombieYouWantToRemove))

SpriteKit - Class inheritance

You can subclass the SKSpriteNode instead of SKNode.

class Enemy : SKSpriteNode
{
var health : Int = 0
var enemyName : String = ""

func moveEnemy() {

}
}

And use it like this

var monster = Enemy(imageNamed: "Enemy-Sprite1")
monster.position = CGPointMake(10 , 400)
monster.enemyName = "enemy1"
monster.health = 4
self.addChild(monster)


Related Topics



Leave a reply



Submit