iosasynchronoussprite-kitskaction

Stop an action that runs forever, run another acton and resume the stopped action in SpriteKit


I'm replicating the Chrome dinosaur game for a class project, and I ran into a problem where I have difficulties stopping the dinosaur from running when it jumps.

I have three textures in a SKTextureAtlas: walk_1, walk_2 and jump, and I want the dinosaur to keep switching between walk_1 and walk_2 so it looks like it is walking. When I touch the screen, the dinosaur jumps, stops walking and the texture is set to jump until it falls on the ground again, and it starts walking again.

However, when I touch the screen, the dinosaur does jump, but the texture is not changed and it keeps walking.

I've tried all sort of solutions like combining removeAction and run, setting moveSpeed to 0 and 1 and simply running the jump sequence in touchesBegan without any additional settings, but none of the above works.

My classmate tells me to use DispatchQueue.main.asyncAfter and surprisingly, it works! But I'm not quite sure why it works and want to know if there's any other solution that can achieve the same goal.

import SpriteKit

class GameScene: SKScene {

    var dinoTexture:SKTextureAtlas!
    var dinosaur:SKSpriteNode!
    var walkForever:SKAction!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(size: CGSize) {
        super.init(size: size)
        dinoTexture = SKTextureAtlas(named: "dinosaur")
    }

    override func didMove(to view: SKView) {

        let walkOnce = SKAction.animate(with: [dinoTexture.textureNamed("walk_1.png"), dinoTexture.textureNamed("walk_2.png")], timePerFrame: 0.1)
        walkForever = SKAction.repeatForever(walkOnce)

        dinosaur = SKSpriteNode(texture: dinoTexture.textureNamed("walk_2.png"))

        dinosaur.run(walkForever, withKey: "dinosaurWalk")

        self.addChild(dinosaur)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            dinosaur.removeAction(forKey: "dinosaurWalk")
            dinosaur.run(SKAction.sequence([SKAction.setTexture(dinoTexture.textureNamed("jump.png")), SKAction.moveBy(x: 0, y: 150, duration: 0.5), SKAction.moveBy(x: 0, y: -150, duration: 0.5), SKAction.setTexture(dinoTexture.textureNamed("walk_2.png"))]))
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                self.dinosaur.run(self.walkForever, withKey: "dinosaurWalk")
        }
    }

Solution

  • SKActions in Spritekit have a built in completion handler when they are run. so no need to dispatch queue.

    I've broken the actions into variables for easier reading, and in my opinion jump down should be faster than jump up

    to access a completion of an SKAction the put your follow up code in squiggly brackets after the run command

    self.run(someAction) {
        //run some code after someAction has run
    }
    

    all you need to do is

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
        coco.removeAction(forKey: "cocoWalk")
    
        let setJumpTexture = SKAction.setTexture(SKTexture(imageNamed: "coco_jump.png"))
        let setWalkTexture = SKAction.setTexture(SKTexture(imageNamed: "coco_walk2.png"))
        let jumpUp = SKAction.moveBy(x: 0, y: 150, duration: 0.5)
        let jumpDown = SKAction.moveBy(x: 0, y: -150, duration: 0.25)
    
        coco.run(SKAction.sequence([setJumpTexture, jumpUp, jumpDown, setWalkTexture])) {
            self.coco.run(self.walkForever, withKey: "cocoWalk")
        }
    }