I will show a few things here for you, and some more for the future readers, so they will have a workable example by just copy-pasting this code. These few things are next:
1. Creating a timer using SKAction
2. Pausing an action
3. Pausing a node itself
4. And as I said, a few things more :)
Note that all of these can be done in a different ways, even simpler than this (when it comes to pausing of actions and nodes) but I will show you detailed way, so you can chose works best for you.
Initial Setup
We have a hero node, and an enemy node. Enemy node will spawn every 5 seconds at the top of the screen and will go downwards, towards the player to poison him.
As I said, we are going to use only SKActions
, no NSTimer
, not even the update:
method. Pure actions. So, here, the player will be static at the bottom of the screen (purple square) and the enemy (red square) will, as already mentioned, travel towards the player and will poison him.
So lets see some code. We need to define usual stuff for all this to work, like setting up physics categories, initialization and positioning of nodes. Also we are going to set things like enemy spawning delay (8 seconds) and poison duration (3 seconds):
//Inside of a GameScene.swift
let hero = SKSpriteNode(color: .purple , size: CGSize(width: 50, height: 50))
let button = SKSpriteNode(color: .yellow, size: CGSize(width: 120, height:120))
var isGamePaused = false
let kPoisonDuration = 3.0
override func didMove(to view: SKView) {
super.didMove(to: view)
self.physicsWorld.contactDelegate = self
hero.position = CGPoint(x: frame.midX, y:-frame.size.height / 2.0 + hero.size.height)
hero.name = "hero"
hero.physicsBody = SKPhysicsBody(rectangleOf: hero.frame.size)
hero.physicsBody?.categoryBitMask = ColliderType.Hero.rawValue
hero.physicsBody?.collisionBitMask = 0
hero.physicsBody?.contactTestBitMask = ColliderType.Enemy.rawValue
hero.physicsBody?.isDynamic = false
button.position = CGPoint(x: frame.maxX - hero.size.width, y: -frame.size.height / 2.0 + hero.size.height)
button.name = "button"
addChild(button)
addChild(hero)
startSpawningEnemies()
}
There is also variable called isGamePaused
which I will comment more later, but as you can imagine, its purpose is to track if game is paused and its value changes when user taps big yellow square button.
Helper Methods
I've made a few helper methods for node creation. I have a feeling that this is not required for you personally, because you looks like you have a good understandings of programming, but I will make it for completeness and for the future readers. So this is the place where you setup things like node's name , or its physics category... Here is the code:
func getEnemy()->SKSpriteNode{
let enemy = SKSpriteNode(color: .red , size: CGSize(width: 50, height: 50))
enemy.physicsBody = SKPhysicsBody(rectangleOf: enemy.frame.size)
enemy.physicsBody?.categoryBitMask = ColliderType.Enemy.rawValue
enemy.physicsBody?.collisionBitMask = 0
enemy.physicsBody?.contactTestBitMask = ColliderType.Hero.rawValue
enemy.physicsBody?.isDynamic = true
enemy.physicsBody?.affectedByGravity = false
enemy.name = "enemy"
return enemy
}
Also, I separated creating of an enemy with its actual spawning. So creating here means create, setup, and return a node which will be later added to a node tree. Spawning means use previously created node add it to a scene, and run action (moving action) to it, so it can move towards the player:
func spawnEnemy(atPoint spawnPoint:CGPoint){
let enemy = getEnemy()
enemy.position = spawnPoint
addChild(enemy)
//moving action
let move = SKAction.move(to: hero.position, duration: 5)
enemy.run(move, withKey: "moving")
}
I think that there is no need for going here into about spawning method because it is very simple. Lets go further to the spawning part:
SKAction Timer
Here is a method which will spawn enemies every x seconds. It will be paused every time we pause an action associated with a "spawning" key.
func startSpawningEnemies(){
if action(forKey: "spawning") == nil {
let spawnPoint = CGPoint(x: frame.midX, y: frame.size.height / 2.0 - hero.size.height)
let wait = SKAction.wait(forDuration: 8)
let spawn = SKAction.run({[unowned self] in
self.spawnEnemy(atPoint: spawnPoint)
})
let sequence = SKAction.sequence([spawn,wait])
run(SKAction.repeatForever(sequence), withKey: "spawning")
}
}
After the node is spawned, it will eventually collide (more precisely, it will make a contact) with a hero. And this is where physics engine comes into play...
Detecting contacts
While enemy is traveling, it will eventually reach the player, and we will register that contact:
func didBegin(_ contact: SKPhysicsContact) {
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch contactMask {
case ColliderType.Hero.rawValue | ColliderType.Enemy.rawValue :
if let projectile = contact.bodyA.categoryBitMask == ColliderType.Enemy.rawValue ? contact.bodyA.node : contact.bodyB.node{
projectile.removeAllActions()
projectile.removeFromParent()
addPoisionEffect(atPoint: hero.position)
}
// Handle more cases here
default : break
//Some other contact has occurred
}
}
Contact detection code is borrowed from here (from author Steve Ives).
I would not go into how contact handling in SpriteKit works, because I would go too much into off-topic that way. So when contact between hero and a projectile is registered, we are doing few things:
1. Stop all actions on projectile so it will stop moving. We could do this by stopping a moving action directly and I will show you later how to do that.
2. Removing a projectile from a parent, because we don't need it anymore.
3. Adding poisoning effect by adding emitter node (I made that effect in particle editor using Smoke template).
Here is the relevant method for the step 3:
func addPoisionEffect(atPoint point:CGPoint){
if let poisonEmitter = SKEmitterNode(fileNamed: "poison"){
let wait = SKAction.wait(forDuration: kPoisonDuration)
let remove = SKAction.removeFromParent()
let sequence = SKAction.sequence([wait, remove])
poisonEmitter.run(sequence, withKey: "emitAndRemove")
poisonEmitter.name = "emitter"
poisonEmitter.position = point
poisonEmitter.zPosition = hero.zPosition + 1
addChild(poisonEmitter)
}
}
As I said, I will mention some things that are not important for your question, but are crucial when doing all this in SpriteKit
. SKEmitterNode
is not removed when emitting is done. It stays in a node tree and eat up resources (at some percent). That is why you have to remove it by yourself. You do this by defining action sequence of two items. First is an SKAction
which waits for a given time (until emitting is done) and second item would be an action which will remove an emitter from its parent when time comes.
Finally - Pausing :)
The method responsible for pausing is called togglePaused()
and it toggles game's paused state based on isGamePaused
variable when yellow button is tapped:
func togglePaused(){
let newSpeed:CGFloat = isGamePaused ? 1.0 : 0.0
isGamePaused = !isGamePaused
//pause spawning action
if let spawningAction = action(forKey: "spawning"){
spawningAction.speed = newSpeed
}
//pause moving enemy action
enumerateChildNodes(withName: "enemy") {
node, stop in
if let movingAction = node.action(forKey: "moving"){
movingAction.speed = newSpeed
}
}
//pause emitters by pausing the emitter node itself
enumerateChildNodes(withName: "emitter") {
node, stop in
node.isPaused = newSpeed > 0.0 ? false : true
}
}
What is happening here is actually simple: we stop spawning action by grabbing it using previously defined key (spawning), and in order to stop it we set action's speed to zero. To unpause it we will do the opposite - set actions speed to 1.0. This applies to the moving action as well, but because many nodes can be moved we enumerate through all of the nodes in a scene.
To show you a difference, I pause SKEmitterNode
directly, so there is one more way for you to pause things in SpriteKit. When the node is paused, all its actions and actions of its children is paused as well.
What is left to mention is that I detect in touchesBegan
if button is pressed, and run togglePaused()
method every time, but I think that code is not really needed.
Video example
To make a better example I have recorded a whole thing. So when I hit the yellow button, all actions will be stopped. Means spawning, moving and poison effect if present will be frozen. By tapping again, I will unpause everything. So here is the result:
Here you can (clearly?) see that when an enemy hits a player, I pause the whole thing , say 1-1.5 seconds after the hit occurred. Then I wait for like 5 seconds or so, and I unpause everything. You can see that emitter continues with emitting for a second or two, and then it disappears.
Note that when an emitter is unpaused, it doesn't look like that it was really unpaused :), but rather looks like that particles were emitting even the emitter is paused (which actually true). This is a bug on iOS 9.1 and I am still on iOS 9.1 on this device :) So in iOS 10, it is fixed.
Conclusion
You don't need NSTimer
for this kind of things in SpriteKit because SKActions
are meant for this. As you can see, when you pause the action, a whole thing will stop. Spawning is stopped, moving is stopped, just like you asked... I have mentioned that there is an easier way to do all this. That is, using a container node. So if all of your nodes were in one container, all nodes, actions and everything will be stopped just by pausing the container node. Simple as that. But I just wanted to show you how yo