Search code examples
iosswiftsprite-kitskphysicsbodysktextureatlas

How can I animate a SKPhysicsBody texture from an SKTextureAtlas


I have an SKTextureAtlas with 7 frames of an animated sprite with transparent background.

I have been able to use a single frame from the atlas as the boundaries of the physics body, but I would like to know how, if it is possible, to have the physics body update its outline for each frame in the atlas.

Here I have the physics body using the frame called "Run0", and it is applied correctly. I just wanted to see if it's possible, or whether it would be completely impractical to have the physics body use each frame from "Run0" to "Run7" in the atlas.

Image of physics body with the outline of "Run0"

This is the code I am working on:

import SpriteKit

class GameScene: SKScene {

let dogSpriteNode = SKSpriteNode(imageNamed: "Run0")
var dogFrames = [SKTexture]()

override func didMove(to view: SKView) {

    physicsWorld.gravity = CGVector(dx: 0, dy: -9.8)
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)

    dogSpriteNode.setScale(0.1)
    dogSpriteNode.position = CGPoint(x: frame.midX, y: frame.midY)
    addChild(dogSpriteNode)

    let textureAtlas = SKTextureAtlas(named: "Dog Frames")

    for index in 0..<textureAtlas.textureNames.count {
        let textureName = "Run" + String(index)
        dogFrames.append(textureAtlas.textureNamed(textureName))
    }

    dogSpriteNode.physicsBody = SKPhysicsBody(
        texture: textureAtlas.textureNamed("Run0"),
        alphaThreshold: 0.5,
        size: dogSpriteNode.frame.size)

}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    dogSpriteNode.run(SKAction.group([SKAction.repeatForever(SKAction.animate(with: dogFrames, timePerFrame: 0.1))]))
    dogSpriteNode.run(SKAction.applyImpulse(CGVector(dx: 0, dy: 1000), duration: 0.1))

}

}


Solution

  • One way to achieve this is with a custom SKAction:

    extension SKAction {
    
        public class func animateTexturesWithPhysics(_ frames: [(texture: SKTexture, duration: TimeInterval)], repeatForever: Bool=true) -> SKAction {
            var actions: [SKAction] = []
            for frame in frames {
                // define a custom action for each frame
                let customAction = SKAction.customAction(withDuration: frame.duration) { node, _ in
                    // if the action target is a sprite node, apply the texture & physics
                    if node is SKSpriteNode {
                        let setTextureGroup = SKAction.group([
                                SKAction.setTexture(frame.texture, resize: false),
                                SKAction.wait(forDuration: frame.duration),
                                SKAction.run {
                                    node.physicsBody = SKPhysicsBody(texture: frame.texture, alphaThreshold: 0.5, size: frame.texture.size())
                                    // add physics attributes here
                                }
                            ])
                        node.run(setTextureGroup)
                    }
                }
                actions.append(customAction)
            }
    
            // add the repeating action
            if (repeatForever == true) {
                return SKAction.repeatForever(SKAction.sequence(actions))
            }
            return SKAction.sequence(actions)
        }
    }
    

    To implement it, you'll need to create an array of frames + durations and apply the action to the sprite:

    typealias Frame = (texture: SKTexture, duration: TimeInterval)
    let timePerFrame: TimeInterval = 0.1
    let dogFrames: [Frame] = dogTextures.map {
        return ($0, timePerFrame)
    }
    
    dogSpriteNode = SKSpriteNode(texture: dogFrames.first)
    let dogAnimationAction = SKAction.animateTexturesWithPhysics(dogFrames)
    dogSpriteNode.run(dogAnimationAction)