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 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))
}
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.
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()
}
}
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
Convert String to Nsdate in Swift
Take Screenshot of Host App Using iOS Share/Action Extensions
Connecting Hc-05 with iPhone Se iOS(V11.0)
How to Parse Firestore Fieldvalue to Date in Swift
Referencing Self in Super.Init
Why Mark Something Final in Swift Except for Architectural Considerations
Making a Http Post Request in Swift 2
Swift For-In Loop with Enumerate on Custom Array2D Class
How How to Perform Multiple Alamofire Requests That Are Finished One After Another
Instance Member Cannot Be Used on Type | Closures
Response Struct Does Not Like Codingkeys
How to Avoid Duplicate Key Error in Swift When Iterating Over a Dictionary
iOS Swift - Uitableviewcell Custom Subclass Not Displaying Content
Round Currency Closest to Five
Using "Codable" to Set Property Values Doesn't Work Through Inheritance