Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
590 views
in Technique[技术] by (71.8m points)

ios - "Pausing" the Game in Swift

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!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 yo


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...