Mir
Mir

Reputation: 137

SpriteKit game freezing after switching scenes multiple times

One of my sprites is freezing after switching between the game scene and game over scene multiple times. Once I die and restart about 6-7 times, my 'enemy sprite' no longer respond to the tilt of my device (if it does, it takes a very long time). My player can still move around just fine when using the onscreen joystick. The whole time my FPS reads 60.

class GameScene: SKScene, SKPhysicsContactDelegate {

    let joyStickSprite = SKSpriteNode(imageNamed: "flatLight09")
    let playerSprite = SKSpriteNode(imageNamed: "p3_front")
    let enemySprite = SKSpriteNode(imageNamed: "elementExplosive001")
    let coinSprite = SKSpriteNode(imageNamed: "gold_1")
    var left = false
    var right = false


    var enemyCount = 0
    var randomNextCoin = Int(arc4random_uniform(5)+1)

    var motionManager = CMMotionManager()
    var destY:CGFloat  = 0.0

    struct CollisionCategoryBitmask {
        static let Player: UInt32 = 0x00
        static let Enemy: UInt32 = 0x01
        static let Floor: UInt32 = 0x02
        static let Coin: UInt32 = 0x03
    }

    var background = SKSpriteNode(imageNamed: "blue_land")

    override func didMoveToView(view: SKView) {

        self.physicsWorld.contactDelegate = self


        createBackground()
        createPlayer()
        createJoyStick()
        createScreenBorder()
        createEnemy()


        motionManager.accelerometerUpdateInterval = 0.1
        motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue(), withHandler: {
            (accelerometerData: CMAccelerometerData!, error: NSError!) in


                var currentY = self.enemySprite.position.y

                let acceleration = accelerometerData.acceleration
                self.destY = (CGFloat(acceleration.y) * 0.75) + (self.destY * 0.25)

            })
        }



    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        /* Called when a touch begins */



        for touch in (touches as! Set<UITouch>) {
            let location = touch.locationInNode(self)

            let touchedNode = self.nodeAtPoint(location)

            if let name = touchedNode.name
            {
                if name == "joyStick"
                {

                    if location.x < joyStickSprite.position.x {
                        left = true
                        right = false
                    }

                    else {
                        right = true
                        left = false
                    }
                }
            }
        }

    }

    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {

        left = false
        right = false
    }

    func presentGameOver () {

        removeAllChildren()

        let newScene = GameScene(size: size)
        newScene.scaleMode = scaleMode
        let reveal = SKTransition.flipHorizontalWithDuration(0.5)
        view?.presentScene(newScene, transition: reveal)
        enemySprite.physicsBody?.dynamic = false
    }

    func createBackground () {

        background.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
        background.size.height = self.size.height

        addChild(background)

    }


    func createPlayer () {

        playerSprite.position = CGPoint(x: self.size.width / 2, y: playerSprite.size.height/2)
        playerSprite.physicsBody = SKPhysicsBody(circleOfRadius: playerSprite.size.width / 2)
        playerSprite.physicsBody?.dynamic = false
        playerSprite.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Player
        playerSprite.physicsBody?.collisionBitMask = 0
        playerSprite.physicsBody?.contactTestBitMask = CollisionCategoryBitmask.Enemy | CollisionCategoryBitmask.Coin


        addChild(playerSprite)

    }

    func movePlayerLeft () {
        let moveLeft = SKAction.moveByX(-10, y: 0, duration: 1)
        playerSprite.runAction(moveLeft)
    }

    func movePlayerRight () {
        let moveRight = SKAction.moveByX(10, y: 0, duration: 1)
        playerSprite.runAction(moveRight)
    }

    func createEnemy () {

        var randomX = Int(arc4random_uniform(600))
        var randomXCG = CGFloat(randomX)


        enemyCount += 1

        enemySprite.position = CGPoint(x: randomXCG, y: self.size.height)
        enemySprite.physicsBody = SKPhysicsBody(circleOfRadius: enemySprite.size.width / 2)
        enemySprite.physicsBody?.dynamic = true
        enemySprite.physicsBody?.allowsRotation = true
        enemySprite.physicsBody?.restitution = 0.0
        enemySprite.physicsBody?.friction = 0.0
        enemySprite.physicsBody?.angularDamping = 0.0
        enemySprite.physicsBody?.linearDamping = 0.0
        enemySprite.physicsBody?.affectedByGravity = true

        enemySprite.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Enemy
        enemySprite.physicsBody?.collisionBitMask = CollisionCategoryBitmask.Floor


        println("enemey count \(enemyCount)")
        println("next coin \(randomNextCoin)")

        addChild(enemySprite)
    }

    func createCoins () {

        var randomX = Int(arc4random_uniform(600))
        var randomXCG = CGFloat(randomX)

        randomNextCoin = Int(arc4random_uniform(10))
        enemyCount = 0

        coinSprite.size.height = playerSprite.size.height/2
        coinSprite.size.width = coinSprite.size.height
        coinSprite.position = CGPoint(x: randomXCG, y: self.size.height)
        coinSprite.physicsBody = SKPhysicsBody(circleOfRadius: enemySprite.size.width/2)
        coinSprite.physicsBody?.dynamic = true
        coinSprite.physicsBody?.affectedByGravity = true
        coinSprite.physicsBody?.categoryBitMask = CollisionCategoryBitmask.Coin

        addChild(coinSprite)
    }


    func createJoyStick () {

        joyStickSprite.setScale(0.4)
        joyStickSprite.position = CGPoint(x: self.size.width/1.1, y: joyStickSprite.size.height)
        joyStickSprite.name = "joyStick"
        joyStickSprite.userInteractionEnabled = false

        addChild(joyStickSprite)
    }

    func updateEnemyPosition () {

        if enemySprite.size.height > enemySprite.position.y {

            enemySprite.position.x =  enemySprite.position.x + destY*20
        }


    }

    func didBeginContact(contact: SKPhysicsContact) {

        let firstNode = contact.bodyA.node as! SKSpriteNode
        let secondNode = contact.bodyB.node as! SKSpriteNode

        if (contact.bodyA.categoryBitMask == CollisionCategoryBitmask.Player) &&
            (contact.bodyB.categoryBitMask == CollisionCategoryBitmask.Enemy) {

                let transition = SKTransition.revealWithDirection(SKTransitionDirection.Down, duration: 1.0)

                let scene = SecondScene(size: self.scene!.size)
                scene.scaleMode = SKSceneScaleMode.AspectFill

                self.scene!.view!.presentScene(scene, transition: transition)
                }

        if (contact.bodyA.categoryBitMask == CollisionCategoryBitmask.Player) &&
            (contact.bodyB.categoryBitMask == CollisionCategoryBitmask.Coin) {

                coinSprite.removeFromParent()
        }
    }

    func createScreenBorder () {


        // 1. Create a physics body that borders the screen
        let borderBody = SKPhysicsBody(edgeFromPoint: CGPointMake(0.0, 0.0), toPoint: CGPointMake(self.size.width, 0.0))
        // 2. Set the friction of that physicsBody to 0
        borderBody.friction = 0

        borderBody.categoryBitMask = CollisionCategoryBitmask.Floor

        // 3. Set physicsBody of scene to borderBody
        self.physicsBody = borderBody
    }



    override func update(currentTime: CFTimeInterval) {

        //detect where on joystick player is touching
        if left == true {
            movePlayerLeft()
        }

        if right == true {
            movePlayerRight()
        }

        //move player to other side when going off screen
        if playerSprite.position.x < -20.0 {
            playerSprite.position = CGPoint(x: self.size.width + 20.0, y: playerSprite.position.y)
        } else if (playerSprite.position.x > self.size.width + 20.0) {
            playerSprite.position = CGPoint(x: -20.0, y: playerSprite.position.y)
        }


        //remove enemeny if off screen
        if enemySprite.position.x < -20.0 || enemySprite.position.x > self.size.width + 20.0 {
            self.enemySprite.removeFromParent()
            createEnemy()
        }

        if randomNextCoin == enemyCount {
            println("coin dropped")
            coinSprite.removeFromParent()
            createCoins()
        }

        updateEnemyPosition()


    }
}

Does anyone have any suggestions?

Upvotes: 1

Views: 472

Answers (1)

0x141E
0x141E

Reputation: 12753

By referencing self.destY in the motionManager.startAccelerometerUpdatesToQueue closure, you are creating a strong reference cycle. From the docs,

If you assign a closure to a property of a class instance, and the closure captures that instance by referring to the instance or its members, you will create a strong reference cycle between the closure and the instance.

This reference cycle is preventing your scene from being released. And since you do not stop the motion manager, the old managers (plural) are still running when you transition scenes. This is likely causing the current scene to freeze after multiple transitions.

Swift uses capture lists to avoid strong reference cycles, where a capture list has the form

{ [ /* weak or unowned + object, ...*/ ]
     /* parameters */ in

}

You can define the capture in a closure as unowned or weak. From the docs,

Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

and

Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.

Here's an example of how to add a capture list to your accelerometer handler:

    motionManager.accelerometerUpdateInterval = 0.1
    motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue.currentQueue()) {
        [unowned self] accelerometerData, error in

            var currentY = self.enemySprite.position.y
            let acceleration = accelerometerData.acceleration
            self.destY = (CGFloat(acceleration.y) * 0.75) + (self.destY * 0.25)
    }

Lastly, it's a good idea to stop the accelerometer updates before transitioning scenes

    override func willMoveFromView(view: SKView) {
        motionManager.stopAccelerometerUpdates()
    }

Upvotes: 2

Related Questions