多次切换场景后 SpriteKit 游戏卡死

Posted

技术标签:

【中文标题】多次切换场景后 SpriteKit 游戏卡死【英文标题】:SpriteKit game freezing after switching scenes multiple times 【发布时间】:2015-09-07 20:24:15 【问题描述】:

我的一个精灵在游戏场景和游戏结束场景之间多次切换后冻结。一旦我死亡并重新启动大约 6-7 次,我的“敌人精灵”不再响应我设备的倾斜(如果有,则需要很长时间)。使用屏幕操纵杆时,我的播放器仍然可以正常移动。我的 FPS 一直是 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()


    

有人有什么建议吗?

【问题讨论】:

【参考方案1】:

通过在motionManager.startAccelerometerUpdatesToQueue 闭包中引用self.destY,您正在创建一个强引用循环。从文档中,

如果您将闭包分配给类实例的属性,并且闭包通过引用实例或其成员来捕获该实例,您将在闭包和实例之间创建一个强引用循环。

此引用循环会阻止您的场景被释放。而且由于您没有停止运动管理器,因此在您转换场景时,旧的管理器(复数)仍在运行。这可能会导致当前场景在多次转换后冻结。

Swift 使用捕获列表来避免强引用循环,其中捕获列表具有以下形式

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


您可以将闭包中的捕获定义为unownedweak。从文档中,

当闭包和它捕获的实例总是相互引用时,将闭包中的捕获定义为无主引用,并且总是同时被释放。

相反,当捕获的引用在未来某个时间点可能变为 nil 时,将捕获定义为弱引用。弱引用始终是可选类型,并在它们引用的实例被释放时自动变为 nil。这使您能够检查它们是否存在于闭包的主体中。

以下是如何将捕获列表添加到加速度计处理程序的示例:

    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)
    

最后,在转换场景之前停止加速度计更新是个好主意

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

【讨论】:

太棒了。非常感谢。这让世界变得与众不同,我非常感谢您解释其背后的思考和推理。

以上是关于多次切换场景后 SpriteKit 游戏卡死的主要内容,如果未能解决你的问题,请参考以下文章

如何在另一个视图控制器中访问 SpriteKit 场景的变量?

将游戏中心添加到游戏后第一次接触时 Spritekit 游戏场景停止

如何使用 Swift 正确切换 SpriteKit 中的 SKScene?

Swift,SpriteKit:释放游戏场景并重新分配新场景

如何有效地暂停 spritekit 中的游戏?

Spritekit 动画加载时间