1

I created a game in Swift that involves monsters appearing. Monsters appear, and disappear, based on timers using something like this:

func RunAfterDelay(_ delay: TimeInterval, block: @escaping ()->()) 
{
    let time = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)

    DispatchQueue.main.asyncAfter(deadline: time, execute: block)
}

and then I'd just call it like this (for example to spawn after 2 seconds):

///Spawn Monster
RunAfterDelay(2) { 
                [unowned self] in
                self.spawnMonster()
 }

I then do something similar for hiding (after x seconds, I despawn the monster).

So I created a settings icon at the top of the screen, and when you tap it, a giant rectangular window appears to change game settings, but naturally the problem is the monsters still spawn in the background. If I whisk the player away to another screen, I believe i'll lose all my game state and can't come back to it without starting all over (the player might be in the middle of their game).

Is there a way to tell all game timers I've created in the above, i.e.

DispatchQueue.main.asyncAfter(deadline: time, execute: block)

To pause and resume when I say so? I guess it's fine to do it with all timers (if there isn't a way to label and pause certain timers).

Thanks!

NullHypothesis
  • 3,808
  • 3
  • 28
  • 72
  • 1
    Instead of using GCD, why not just use an SKAction sequence with 2 SKActions in it: a timer, and a closure that spawns the enemy? I would have set up an enum for pause or not paused and clear the SKAction from occurring when paused, and reinstate the SKAction when unpaused. – Jose Ramirez Dec 11 '16 at 09:46

2 Answers2

2

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:

video

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 you can grab an action by a key, or pause the node, or change its speed... Hope this helps and make sense!

Community
  • 1
  • 1
Whirlwind
  • 13,478
  • 6
  • 48
  • 124
  • hey thanks for taking the time to do this; I'm sure it wasn't something you whipped up in 5 min, and for that I am really grateful. I will read this and absorb as much as I can :) One question, though - Let's say you wanted to poison the player, but without anything from SpriteKit (the player class has a property as an array of effects on him, and each effect has a timer on it for how long it lasts). When you pause the game, does the poison effect stop in this case (based on your code)? I see you added a poisonEmitted, but is that the reason that it pauses? b/c it's a SpriteKit object? TY!! – NullHypothesis Dec 17 '16 at 03:21
  • Say that you have a property called `isPoisoned` on a player class set to `false` by default. To poison the player, you would run a sequence as in my code, but instead of adding an emitter, you would set this variable to true (using `runBlock`), and instead of removing an emitter from its parent, you would set `isPoisoned` to false. To pause the effect, you pause the player or the action. I don't know what is the logic with the array you've mentioned (what type is it and everything), but, the same logic applies. When player got poisoned, you update an array (using actions) appropriately… – Whirlwind Dec 17 '16 at 09:45
  • If you are unclear about what I am saying, feel free to ask, because sometimes it takes time to switch to `SKActions` way of thinking, but when you get used to them all this will be an easy task for you. – Whirlwind Dec 17 '16 at 09:46
  • ok thank you, I am going to try this tomorrow :) Do you think doing it the original way I mentioned could pose some sort of a problem with memory and memory crashes on an actual device btw? i.e. timer adds overhead – NullHypothesis Dec 22 '16 at 05:28
1

I have solved this and would like to share my hours worth of research/coding in the conclusion below. To restate the problem more simply, I actually wanted to achieve this (not simply using the SpriteKit scene pause, which is quite easy):

  1. Start one or more timers in Swift
  2. Stops all timers (when the user presses pause)
  3. When the user unpauses, all timers starts again, where they left off

Someone had mentioned to me that because I am using DispatchQueue.main.asyncAfter there is no way to pause/stop in the way I want (you can cancel but I digress). This makes sense, after all i'm doing an asyncAfter. But to actually get a timer going, you need to use NSTimer (now in Swift3 it is called Timer).

After researching, I see this actually not possible to pause/unpause so you "cheat" by creating a new timer (for each one) when you want to restart paused timers. My conclusion to do this is as follows:

  1. When each timer starts, record your delay you need (we access this latter) and also record the time that this timer will "fire". So for example if it starts in 3 seconds, and executes code, then record the time as Date() + 3 seconds. I achieve this using this code:
//Take the delay you need (delay variable) and add this to the current time

let calendar = Calendar.current        
let YOUR_INITIAL_TIME_CAPTURED = calendar.date(byAdding: .nanosecond, value: Int(Int64(delay * Double(NSEC_PER_SEC))), to: Date())!
  1. Now that you've recorded the time your timer will fire, you can wait for the user to press stop. When they do, you will invalidate each timer with .invalidate() and immediately record the stopped time. In fact, at this point, you can also completely calculate the remaining delay needed when the user starts as:
//Calculate the remaining delay when you start your timer back
let elapsedTime = YOUR_INITIAL_TIME_CAPTURED.timeIntervalSince(Date)
let remainingDelay = YOUR_INITIAL_TIMER_DELAY - elapsedTime
  1. When the user taps start, you can start all timers again by simply creating new ones, utilizing the aforementioned remainder (remainingDelay) and viola` you have your new timers.

Now because I had multiple timers, I decided I needed to create a dictionary in my AppDelegate (accessed via a service class) to retain all my active timers. Whenever a timer ended, I would remove it from the dictionary. I ended up making a special class that had properties for the timer, the initial delay, and the time it started. Technically I could've used an array and also put the timer key on that class, but I digress..

I created my own addTimer method that would create a unique key for each timer and then when the timer's code finished, it would self-remove as follows:

  let timerKey = UUID().uuidString

let myTimer: Timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) {
            _ in
               block()
               self.timers.removeValue(forKey: timerKey)
            }

        }

Note: block() is simply calling whatever block you wrap in your timer. For example I did something cool like this:

addTimer(delay: 4, repeating: true)
        { [unowned self] in
            self.spawnMonster()
        }

So addTimer would run the self.spawnMonster code (as block()) and then it would self-remove from the dictionary when done.

I got way more sophisticated later, and did things like keep repeating timers running and not self-removing, but it's just a lot of very specific code for my purposes and probably would consume way too much of this reply :)

Anyway I really hope this helps someone, and would love to answer any questions that anyone has. I spent a lot of time on this!

Thanks!

NullHypothesis
  • 3,808
  • 3
  • 28
  • 72
  • Now you would rather do all that instead of using SKActions which have all that implemented by default? I dont get it. Also, whatever you do with NSTimer, SKActions are capable to do the same in a more elegant way and are meant for time related actions in SpriteKit :) – Whirlwind Dec 15 '16 at 00:18
  • Thanks for responding! :) SKActions can't run on a timer that can be paused, though, right? I might want to make the player poisoned (every second he takes -health damage) or make him invincible for 10 seconds. I want those effects to pause, as well as the monsters pausing. Everything is now nicely under one umbrella, and when I pause, all those timers stop! – NullHypothesis Dec 15 '16 at 04:07
  • Actually I dont see your point here. That is certainly possible with actions and that is how they work. Say you want player invincible for some time. Just set a bool property on a player, and run action which will fire after 10 seconds to change this value (and apply visual effects if needed). So it would be an action sequence with a delay of 10 seconds and a (completion) block? If you pause the game on a button, you can access that particular action through its key and pause it (or pause player node... or pause the container node ...) and everythig will stop. I dont see a problem here :) – Whirlwind Dec 15 '16 at 04:16
  • The point is, forget NSTimer. SKActions can do all that. In this case SKAction sequence will act as a timer. That timer will execute something (other actions or block of a code) every n seconds (and will be paused automatically or manually depending on your game needs). If you need an example for any of this I can make my answer later today when I get my hands on computer. – Whirlwind Dec 15 '16 at 11:56
  • sure i'd love to see you do that. I don't believe it does that the way I wanted, but would love to see it. Basically two things going on: a player gets randomly poisoned, and loses 1 health every second but if game is paused the timer stops, but resumes when he unpauses. And also monsters spawn randomly every few seconds, and despawn after 3 seconds. But if you pause halfway through a spawn, the monster should despawn in 1.5 seconds, not instantly (which is what was happening for me). Thanks i'm really excited to see this :) – NullHypothesis Dec 15 '16 at 17:14
  • I guess I could make a way smaller post, but I was like, let make a complete example for future readers. You should just look at pausing part. So it is not really complicated, and it is all about on action sequence with two actions in it. If you have any questions, feel free to ask. – Whirlwind Dec 22 '16 at 09:03
  • Consider changing the "accepted" answer to @Whirlwind's answer – Tyler A. Jan 25 '21 at 18:38