iosswiftsprite-kitskspritenodeskaudionode

Why are there multiple SKSpriteNode and SKAudioNode objects when I only created one of each?


I created an SKSpriteNode object, but multiple objects are created, appearing as strings of them. The sound coming from the SKAudioNode object is also duplicated many times to give a reverberating effect.

Here is the code below. I noticed it doing this when I added the code to set the pinned properties of the physicsBody objects to true in the didBegin callback method.

Here is a screenshot. Notice there are multiple explosion SKSpriteNode objects. I only created one SKSpriteNode object for the explosion.

multiple car explosions

I suspect there is a setting somewhere that I can change to disable this effect.

import UIKit
import SpriteKit

class GameScene: SKScene {

    let player = SKSpriteNode(imageNamed: "player")
    var moveRate: CGFloat!

    override func didMove(to view: SKView) {

        physicsWorld.contactDelegate = self

        if UIDevice.current.userInterfaceIdiom == .phone {
            moveRate = 5
        } else {
            moveRate = 15
        }

        player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
        player.physicsBody?.isDynamic = true
        player.physicsBody?.affectedByGravity = false
        player.physicsBody?.categoryBitMask = 0b00001
//        player.physicsBody?.collisionBitMask = 0b00001
        player.physicsBody?.contactTestBitMask = 0b00001

        player.position = CGPoint(x: 20 + player.size.width/2, y: view.frame.height / 2)

        addChild(player)

        let carEngineStart = SKAudioNode(fileNamed: "car_engine_running")

        addChild(carEngineStart)

        run(SKAction.repeatForever(
            SKAction.sequence([
                SKAction.run(addCompetitor),
                SKAction.wait(forDuration: 4)
                ])
        ))

    }

    override func update(_ currentTime: TimeInterval) {

        let internalRollSign = TrialSpriteKit.sign(internalRoll)

        switch internalRollSign {
        case .zero:
            break
        case .positive:
            if player.position.y < self.size.height {
                player.position.y += moveRate
            }
        case .negative:
            if player.position.y > 0 {
                player.position.y -= moveRate
            }
        }

    }

    enum Car: String, CaseIterable {

        case blue = "blue"
        case green = "green"
        case orange = "orange"
        case purple = "purple"
        case utili = "utili"
        case white = "white"
        case yellow = "yellow"

        static func random<G: RandomNumberGenerator>(using generator: inout G) -> Car {
            return Car.allCases.randomElement(using: &generator)!
        }

        static func random() -> Car {
            var g = SystemRandomNumberGenerator()
            return Car.random(using: &g)
        }

    }

    func random() -> CGFloat {
        return CGFloat(Float(arc4random()) / /* 0xFFFFFFFF */ 4294967296)
    }

    func random(min: CGFloat, max: CGFloat) -> CGFloat {
        return random() * (max - min) + min
    }

    func addCompetitor() {

        // Create sprite
        let carString = Car.random().rawValue
        let car = SKSpriteNode(imageNamed: carString)

        car.physicsBody = SKPhysicsBody(rectangleOf: car.size) // 1
        car.physicsBody?.isDynamic = true
        car.physicsBody?.affectedByGravity = false
//        car.physicsBody?.categoryBitMask = 0b00001
        car.physicsBody?.collisionBitMask = 0b00001
        car.physicsBody?.contactTestBitMask = 0b00001

        // Determine where to spawn the car along the Y axis
        let actualY = random(min: car.size.height/2, max: size.height - car.size.height/2)

        // Position the car slightly off-screen along the right edge,
        // and along a random position along the Y axis as calculated above
        car.position = CGPoint(x: size.width + car.size.width/2, y: actualY)

        // Add the car to the scene
        addChild(car)

        // Determine speed of the car
        let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))

        // Create the actions
        let actionMove = SKAction.move(to: CGPoint(x: -car.size.width/2, y: actualY), duration: TimeInterval(actualDuration))
        let actionMoveDone = SKAction.removeFromParent()
        car.run(SKAction.sequence([actionMove, actionMoveDone]))

    }

}

extension GameScene: SKPhysicsContactDelegate {

    func didBegin(_ contact: SKPhysicsContact) {

        contact.bodyA.pinned = true
        player.physicsBody?.pinned = true

        let explosion = SKSpriteNode(imageNamed: "explosion")

        explosion.position = contact.contactPoint

        addChild(explosion)

        run(
            SKAction.sequence(
                [
                    SKAction.playSoundFileNamed("car_explosion", waitForCompletion: true),
                    SKAction.run({
                        explosion.removeFromParent()
                        contact.bodyB.node?.removeFromParent()
                    }),
                    SKAction.wait(forDuration: 1),
                    SKAction.run({
                        self.player.zRotation = 0
                        self.player.position = CGPoint(x: 20 + self.player.size.width/2, y: self.view!.frame.height / 2)
                        self.player.physicsBody?.pinned = false
                    })
                ]
            )
        )

     }

}

Solution

  • You have the reason for the multiple nodes in your question.

    I noticed it doing this when I added the code to set the pinned properties of the physicsBody objects to true in the didBegin callback method.

    According to the SpriteKit documentation, when you set the pinned property to true and the parent node has a physics body, the two bodies are treated as if they are connected with a pin joint.

    Because the two bodies are connected, contact is being made repeatedly, even if there isn't a collision, resulting in repeated calls to the didBegin function. Each call to didBegin creates a new sprite node. Your game is calling didBegin many more times than you expect so you end up with more sprite nodes than you expect.

    The general solution is to create the explosion sprite node only when there is a collision and only create the sprite node once per collision.