位掩码冲突的间歇性错误

Intermittent errors with Bitmask Collisions

我正在 Swift 中使用 SpriteKit 制作 2D 滚动射击游戏。我已经设置 SKPhysicsBody 并使用位掩码进行碰撞。我不断收到间歇性错误,碰撞会正常工作然后停止工作。我得到的错误是 Fatal error: Unexpectedly found nil while unwrapping an Optional value。我不明白为什么有时我得到零值,而其他时候却得到值。我在游戏中有几个不同的精灵,在进行了大量测试以查看碰撞是否有任何差异之后,我似乎找不到问题所在。例如,玩了几次,我用激光射击了一颗小行星,效果很好。第二天,完全相同的事情使游戏崩溃。另一个示例小行星撞击玩家头部并且工作正常,小行星从侧面撞击玩家导致游戏崩溃但第二天可以正常工作。我不知道问题是否出在我为每个精灵设置 PhysicsBody 的方式上,因为我已经尝试更改它但仍然有问题,或者我的 SKPhysicsContact 设置完全错误。非常感谢任何帮助,谢谢。

我的代码的精简版

import SpriteKit
import GameplayKit
import CoreMotion


@objcMembers
class GameScene: SKScene, SKPhysicsContactDelegate {
    
    //Player Image
    let player = SKSpriteNode(imageNamed: "Player.png")
    
    //Timer to spawn enemies
    var gameTimer:Timer!
    
    //Array for different astroids
    var astroidArray = ["astroid1", "astroid2"]
    
    //Array for differnet enemy ships
    var enemyArray = ["Enemy1"]
    
    //For collision
    let playerCategory:UInt32 = 0x1 << 1
    let playerLaserCategory:UInt32 = 0x1 << 2
    let astroidCategory:UInt32 = 0x1 << 3
    let enemyCategory:UInt32 = 0x1 << 4
    let bossCategory:UInt32 = 0x1 << 5
    
    override func didMove(to view: SKView) {
          
        //Position Player
        player.position.y = -400
        player.zPosition = 1
        addChild(player)
        
        //Player Physics for collision
        //player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
        player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)
        player.physicsBody?.isDynamic = true
        
        player.physicsBody?.categoryBitMask = playerCategory
        player.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory | bossCategory
        //avoid any unwanted collisions
        //player.physicsBody?.collisionBitMask = 0
        
        //Physics for World
        self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
        physicsWorld.contactDelegate = self
        
        //Timer to spawn astroids
        gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addAstroid), userInfo: nil, repeats: true)
        
        //Timer to spawn enemy
        gameTimer = Timer.scheduledTimer(timeInterval: 0.75, target: self, selector: #selector(addEnemy), userInfo: nil, repeats: true)
        
    }
    
    func addAstroid() {
        
        astroidArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: astroidArray) as! [String]
        
        //Select astroid from array
        let astroid = SKSpriteNode(imageNamed: astroidArray[0])
        
        //GameplayKit randomization services to spawn different astroids
        let randomAstroidPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
        //Randomly spawn astroid in different positions
        let position = CGFloat(randomAstroidPosition.nextInt())
        astroid.position = CGPoint(x: position, y: self.frame.size.height + astroid.size.height)
        
        astroid.zPosition = 1
        
        //Astroid Physics for collision
        astroid.physicsBody = SKPhysicsBody(circleOfRadius: astroid.size.width / 2)
      
        astroid.physicsBody?.isDynamic = true
        
        astroid.physicsBody?.categoryBitMask = astroidCategory
        astroid.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
        //avoid any unwanted collisions
        //astroid.physicsBody?.collisionBitMask = 0
        
        addChild(astroid)
        
        //Astroid speed
        let animationDuration:TimeInterval = 6
        
        //Clean up, remove astroids once reached a certain distance
        var actionArray = [SKAction]()
        actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
        actionArray.append(SKAction.removeFromParent())
        
        astroid.run(SKAction.sequence(actionArray))
    }
    
    func addEnemy() {
        
        enemyArray = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: enemyArray) as! [String]
        
        //Select enemy from array
        let enemy = SKSpriteNode(imageNamed: enemyArray[0])
        
        //GameplayKit randomization services to spawn different enemies
        let randomEnemyPosition = GKRandomDistribution(lowestValue: -350, highestValue: 350)
        //Randomly spawn enemy in different positions
        let position = CGFloat(randomEnemyPosition.nextInt())
        enemy.position = CGPoint(x: position, y: self.frame.size.height + enemy.size.height)
        
        enemy.zPosition = 1
        
        //Enemy Physics for collision
        enemy.physicsBody = SKPhysicsBody(circleOfRadius: enemy.size.width / 2)
        enemy.physicsBody?.isDynamic = true
        
        enemy.physicsBody?.categoryBitMask = enemyCategory
        enemy.physicsBody?.contactTestBitMask = playerLaserCategory | playerCategory
        //avoid any unwanted collisions
        //enemy.physicsBody?.collisionBitMask = 0
       
        if score >= 20 {
         addChild(enemy)
        }
        
        //Enemy speed
        let animationDuration:TimeInterval = 6
        
        //Clean up, remove enemy once reached a certain distance
        var actionArray = [SKAction]()
        actionArray.append(SKAction.move(to: CGPoint(x: position, y: -700), duration: animationDuration))
        actionArray.append(SKAction.removeFromParent())
        
        enemy.run(SKAction.sequence(actionArray))
    }
    
    func fireLaser() {
        
        //Sound effect
        self.run(SKAction.playSoundFileNamed("laser.wav", waitForCompletion: false))
        
        //Create and position laser
        let playerLaser = SKSpriteNode(imageNamed: "laser")
        playerLaser.position = player.position
        playerLaser.position.y += 65
        
        //Laser Physics
        playerLaser.physicsBody = SKPhysicsBody(circleOfRadius: playerLaser.size.width / 2)
        playerLaser.physicsBody?.isDynamic = true
        
        playerLaser.physicsBody?.categoryBitMask = playerLaserCategory
        playerLaser.physicsBody?.contactTestBitMask = astroidCategory | enemyCategory
        //avoid any unwanted collisions
        //playerLaser.physicsBody?.collisionBitMask = 0
        playerLaser.physicsBody?.usesPreciseCollisionDetection = true
        
        addChild(playerLaser)
        
        //Animation for laser firing
        let animationDuration:TimeInterval = 0.3
        
        //Clean up, removes laser blast from game
        var actionArray = [SKAction]()
        actionArray.append(SKAction.move(to: CGPoint(x: player.position.x, y: self.frame.size.height), duration: animationDuration))
        actionArray.append(SKAction.removeFromParent())
        
        playerLaser.run(SKAction.sequence(actionArray))
    }
    
    //Function for physics to know what object hit what
    func didBegin(_ contact: SKPhysicsContact) {
        var A:SKPhysicsBody
        var B:SKPhysicsBody
     
        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
            A = contact.bodyA
            B = contact.bodyB
        } else {
            A = contact.bodyB
            B = contact.bodyA
        }
        
        //PlayerLaser is A and Astroid is B
        if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
            playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
        }
        //PlayerLaser is A and Enemy is B
        else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
            playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
        }
        //Player is A and Astroid is B
        else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
            playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
        }
        //Player is A and Enemy is B
        else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
            playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
        }
    }
    
    //Function for playerLaser to destroy Astroid
    func playerLaserHitAstroid (laserNode:SKSpriteNode, astroidNode:SKSpriteNode) {
        
        //Create explosion effect
        let explosion = SKEmitterNode(fileNamed: "Explosion")!
        explosion.position = astroidNode.position
        addChild(explosion)
        
        //Play explosion sound effect
        self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
        
        //remove sprites
        laserNode.removeFromParent()
        astroidNode.removeFromParent()
        
        //Remove explosion effect after a delay
        self.run(SKAction.wait(forDuration: 2)) {
            explosion.removeFromParent()
        }
        print("laser hit astroid")
        
        //Add score
        score += 5
    }
    
    //Function for playerLaser to destroy Enemy
    func playerLaserHitEnemy (laserNode:SKSpriteNode, enemyNode:SKSpriteNode) {
        
        //Create explosion effect
        let explosion = SKEmitterNode(fileNamed: "Explosion")!
        explosion.position = enemyNode.position
        addChild(explosion)
        
        //Play explosion sound effect
        self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
        
        //remove sprites
        laserNode.removeFromParent()
        enemyNode.removeFromParent()
        
        //Remove explosion effect after a delay
        self.run(SKAction.wait(forDuration: 2)) {
            explosion.removeFromParent()
        }
        print("laser hit enemy")
        
        //Add score
        score += 10
    }
    
    //Function for when player and astroid collide
    func playerHitAstroid(playerNode:SKSpriteNode, astroidNode:SKSpriteNode) {
        
        let explosionA = SKEmitterNode(fileNamed: "Explosion")!
        explosionA.position = astroidNode.position
        explosionA.zPosition = 3
        addChild(explosionA)
        
        print("Player hit astroid")
        
//        let explosionB = SKEmitterNode(fileNamed: "Explosion")!
//        explosionB.position = playerNode.position
//        explosionB.zPosition = 3
//        addChild(explosionB)
        
        //Play explosion sound effect
        self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
        
        //remove sprites
        //playerNode.removeFromParent()
        astroidNode.removeFromParent()
        
        //Remove explosion effect after a delay
        self.run(SKAction.wait(forDuration: 2)) {
            explosionA.removeFromParent()
            //explosionB.removeFromParent()
        }
        
        //Removes a life when hit
        if livesArray.count > 0 {
            let lifeNode = livesArray.first
            lifeNode?.removeFromParent()
            livesArray.removeFirst()
        }
        
        //Remove player when all lives are gone
        if livesArray.count == 0 {
            playerNode.removeFromParent()
            let transition = SKTransition.flipHorizontal(withDuration: 0.5)
            let gameOver = GameOverScene(fileNamed: "GameOverScene")!
            gameOver.score = self.score
            gameOver.scaleMode = scaleMode
            self.view?.presentScene(gameOver, transition: transition)
        }
    }
    
    //Function for when player and enemy collide
    func playerHitEnemy(playerNode:SKSpriteNode, enemyNode:SKSpriteNode) {
        
        let explosionA = SKEmitterNode(fileNamed: "Explosion")!
        explosionA.position = enemyNode.position
        explosionA.zPosition = 3
        addChild(explosionA)
        
        print("Player hit enemy")
        
//        let explosionB = SKEmitterNode(fileNamed: "Explosion")!
//        explosionB.position = playerNode.position
//        explosionB.zPosition = 3
//        addChild(explosionB)
    
        //Play explosion sound effect
        self.run(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
        
        //remove sprites
        //playerNode.removeFromParent()
        enemyNode.removeFromParent()
        
        //Remove explosion effect after a delay
        self.run(SKAction.wait(forDuration: 2)) {
            explosionA.removeFromParent()
            //explosionB.removeFromParent()
        }
        
        //Removes a life when hit
        if livesArray.count > 0 {
            let lifeNode = livesArray.first
            lifeNode?.removeFromParent()
            livesArray.removeFirst()
        }
        
        //Remove player when all lives are gone
        if livesArray.count == 0 {
            playerNode.removeFromParent()
            let transition = SKTransition.flipHorizontal(withDuration: 0.5)
            let gameOver = GameOverScene(fileNamed: "GameOverScene")!
            gameOver.score = self.score
            gameOver.scaleMode = scaleMode
            self.view?.presentScene(gameOver, transition: transition)
        }
        
    }
  
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        fireLaser()
    }
}

我想我已经修好了。我会在这里 post 解决方案,以防将来有人遇到与我相同的问题。我有这样的玩家 physicsBody

player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.size)

像这样以一种奇怪的方式工作,正面碰撞效果很好,但侧面的任何撞击都会使游戏崩溃。所以我把它改回原来的方式

player.physicsBody = SKPhysicsBody(rectangleOf: player.size)

我之前更改过这个,因为当一颗小行星靠近但没有被击中时,它会将精灵拖出位置。我通过设置

解决了这个问题
player.physicsBody?.collisionBitMask = 0

现在,碰撞正在按预期进行,或者它们现在正在进行。

我不确定问题是否已解决。当我尝试您的代码时,它崩溃了,因为其中一个接触体节点为零。当从纹理、矩形和圆形创建物理体时,我能够产生崩溃。没关系...问题不在于:)

这是因为您在物理模拟完成之前删除了节点。

看看一帧的样子:

所以发生的事情是你在物理模拟完成之前删除了你的节点,所以物理引擎保留物理体导致它需要完成计算,但是节点被删除了。

因此 didBegin 中为 nil。所以解决方案是创建一个变量来保存要删除的节点:

private var trash:[SKNode] = []

然后在每个有物理体节点的地方执行此操作:

(比如你的 playerHitAsteroid 方法)

trash.append(laserNode)
trash.append(astroidNode)
    
 self.run(SKAction.wait(forDuration: 2)) {[weak self] in
        guard let `self` = self else {return}
        self.trash.append(explosion)
 }

您在应用程序中几乎没有更多地方可以更改此设置。也看看这部分:

if livesArray.count == 0 {
   trash.append(playerNode)
           
   print("Game over")
}

还有更多。但是当你像这样在所有地方修复它时,你就可以通过覆盖 didSimulatePhysics

来实现实际删除
 override func didSimulatePhysics() {
        //first go through every node and remove it from parent
        trash.map { node in
            node.run(SKAction.sequence([SKAction.fadeOut(withDuration: 0.25), SKAction.removeFromParent()]))
        }
        trash.removeAll() // then empty thrash array before next frame
    }

最后你可以像这样改变didBegin,只是为了立即捕获这个错误。如果您遵循以下策略,就不会发生这种情况:

  • 在 didSimulatePhysics 中删除带有物理实体的节点
  • 正确设置位掩码(看起来,你做对了)

也许值得一提的是要小心 Timer。检查 this。这是很久以前的事了,也许有些事情已经改变了,我最近没有测试它,但我仍然更喜欢 update 方法或 SKAction 来进行我游戏中与时间相关的操作。

所以,像这样更改 didBegin

func didBegin(_ contact: SKPhysicsContact) {
        guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else {
            
            fatalError("Physics body without its node detected!")
        }
        let A = contact.bodyA
        let B = contact.bodyB
        
        //PlayerLaser is A and Astroid is B
        if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
            playerLaserHitAstroid(laserNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
        }
        //PlayerLaser is A and Enemy is B
        else if (A.categoryBitMask & playerLaserCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
            playerLaserHitEnemy(laserNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
        }
        //Player is A and Astroid is B
        else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & astroidCategory) != 0 {
            playerHitAstroid(playerNode: A.node as! SKSpriteNode, astroidNode: B.node as! SKSpriteNode)
        }
        //Player is A and Enemy is B
        else if (A.categoryBitMask & playerCategory) != 0 && (B.categoryBitMask & enemyCategory) != 0 {
            playerHitEnemy(playerNode: A.node as! SKSpriteNode, enemyNode: B.node as! SKSpriteNode)
        }
    }

不需要,但我觉得这种实现接触检测的方式(通过切换掩码)有点多readable,如果你想看一下

一个与此节点删除无关的建议...不要使用那么多强制解包! :D