多次调用 SpriteKit 碰撞

Posted

技术标签:

【中文标题】多次调用 SpriteKit 碰撞【英文标题】:SpriteKit Collisions called multiple times 【发布时间】:2019-03-07 16:24:25 【问题描述】:

我是 SpriteKit 的新手,正在通过制作一个飞扬的小鸟风格的游戏来学习。

我正在使用一个鸟节点,其 PhyshicsBody 绑定到纹理。

问题在于,有时当小鸟撞击得分节点时,它会记录多次碰撞。我添加了一个 if 以在第一次调用该函数时删除节点,但我仍在注册多个冲突。

如果使用 if 语句,我会认为这可以防止问题发生,为什么还会发生这种情况?

感兴趣的函数:

 func didBegin(_ contact: SKPhysicsContact) 

        if moving.speed > 0 
            if ( contact.bodyA.categoryBitMask & scoreCategory ) == scoreCategory || ( contact.bodyB.categoryBitMask & scoreCategory ) == scoreCategory 
                // Bird has contact with score entity

                if(contact.bodyB.node != nil && contact.bodyA.node != nil)
                score += 1
                scoreLabelNode.text = String(score)

                    if (contact.bodyA.node?.name == "scoreArea") 
                        contact.bodyA.node?.removeFromParent()
                     else if contact.bodyB.node?.name == "scoreArea" 
                        contact.bodyB.node?.removeFromParent()
                    

                // Add a little visual feedback for the score increment
                scoreLabelNode.run(SKAction.sequence([SKAction.scale(to: 1.5, duration:TimeInterval(0.1)), SKAction.scale(to: 1.0, duration:TimeInterval(0.1))]))
                
             

完整代码:

import SpriteKit

class GameScene: SKScene, SKPhysicsContactDelegate
    let verticalPipeGap = 250.0

    var bird:SKSpriteNode!
    var skyColor:SKColor!
    var pipeTextureUp:SKTexture!
    var pipeTextureDown:SKTexture!
    var movePipesAndRemove:SKAction!
    var moving:SKNode!
    var pipes:SKNode!
    var canRestart = Bool()
    var scoreLabelNode:SKLabelNode!
    var score = NSInteger()
    var contactState = false


    let birdCategory: UInt32 = 1 << 0
    let worldCategory: UInt32 = 1 << 1
    let pipeCategory: UInt32 = 1 << 2
    let scoreCategory: UInt32 = 1 << 3

    override func didMove(to view: SKView) 

        canRestart = true

        // setup physics
        self.physicsWorld.gravity = CGVector( dx: 0.0, dy: -5.0 )
        self.physicsWorld.contactDelegate = self

        // setup background color
        skyColor = SKColor(red: 81.0/255.0, green: 192.0/255.0, blue: 201.0/255.0, alpha: 1.0)
        self.backgroundColor = skyColor

        moving = SKNode()
        self.addChild(moving)
        pipes = SKNode()
        moving.addChild(pipes)

        // ground
        let groundTexture = SKTexture(imageNamed: "land")
        groundTexture.filteringMode = .nearest // shorter form for SKTextureFilteringMode.Nearest

        let moveGroundSprite = SKAction.moveBy(x: -groundTexture.size().width * 2.0, y: 0, duration: TimeInterval(0.02 * groundTexture.size().width * 2.0))
        let resetGroundSprite = SKAction.moveBy(x: groundTexture.size().width * 2.0, y: 0, duration: 0.0)
        let moveGroundSpritesForever = SKAction.repeatForever(SKAction.sequence([moveGroundSprite,resetGroundSprite]))

        for i in 0 ..< 2 + Int(self.frame.size.width / ( groundTexture.size().width * 2 )) 
            let i = CGFloat(i)
            let sprite = SKSpriteNode(texture: groundTexture)
            sprite.setScale(2.0)
            sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0)
            sprite.run(moveGroundSpritesForever)
            moving.addChild(sprite)
        

        // skyline
        let skyTexture = SKTexture(imageNamed: "sky")
        skyTexture.filteringMode = .nearest

        let moveSkySprite = SKAction.moveBy(x: -skyTexture.size().width * 2.0, y: 0, duration: TimeInterval(0.1 * skyTexture.size().width * 2.0))
        let resetSkySprite = SKAction.moveBy(x: skyTexture.size().width * 2.0, y: 0, duration: 0.0)
        let moveSkySpritesForever = SKAction.repeatForever(SKAction.sequence([moveSkySprite,resetSkySprite]))

        for i in 0 ..< 2 + Int(self.frame.size.width / ( skyTexture.size().width * 2 )) 
            let i = CGFloat(i)
            let sprite = SKSpriteNode(texture: skyTexture)
            sprite.setScale(2.0)
            sprite.zPosition = -20
            sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0 + groundTexture.size().height * 2.0)
            sprite.run(moveSkySpritesForever)
            moving.addChild(sprite)
        

        // create the pipes textures
        pipeTextureUp = SKTexture(imageNamed: "PipeUp")
        pipeTextureUp.filteringMode = .nearest
        pipeTextureDown = SKTexture(imageNamed: "PipeDown")
        pipeTextureDown.filteringMode = .nearest

        // create the pipes movement actions
        let distanceToMove = CGFloat(self.frame.size.width + 2.0 * pipeTextureUp.size().width)
        let movePipes = SKAction.moveBy(x: -distanceToMove, y:0.0, duration:TimeInterval(0.01 * distanceToMove))
        let removePipes = SKAction.removeFromParent()
        movePipesAndRemove = SKAction.sequence([movePipes, removePipes])

        // spawn the pipes
        let spawn = SKAction.run(spawnPipes)
        let delay = SKAction.wait(forDuration: TimeInterval(2.0))
        let spawnThenDelay = SKAction.sequence([spawn, delay])
        let spawnThenDelayForever = SKAction.repeatForever(spawnThenDelay)
        self.run(spawnThenDelayForever)

        // setup our bird
        let birdTexture1 = SKTexture(imageNamed: "bird-02")
        birdTexture1.filteringMode = .nearest
        let birdTexture2 = SKTexture(imageNamed: "bird-02")
        birdTexture2.filteringMode = .nearest

        let anim = SKAction.animate(with: [birdTexture1, birdTexture2], timePerFrame: 0.2)
        let flap = SKAction.repeatForever(anim)

        bird = SKSpriteNode(texture: birdTexture1)

        bird.position = CGPoint(x: self.frame.size.width * 0.35, y:self.frame.size.height * 0.6)
        bird.run(flap)


        bird.physicsBody = SKPhysicsBody(texture: bird.texture!,
                                         size: bird.texture!.size())
        bird.setScale(0.4)
        bird.physicsBody?.isDynamic = true
        bird.physicsBody?.allowsRotation = false

        bird.physicsBody?.categoryBitMask = birdCategory
        bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory
        bird.physicsBody?.contactTestBitMask = worldCategory | pipeCategory

        self.addChild(bird)

        // create the ground
        let ground = SKNode()
        ground.position = CGPoint(x: 0, y: groundTexture.size().height)
        ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.size.width, height: groundTexture.size().height * 2.0))
        ground.physicsBody?.isDynamic = false
        ground.physicsBody?.categoryBitMask = worldCategory
        self.addChild(ground)

        // Initialize label and create a label which holds the score
        score = 0
        scoreLabelNode = SKLabelNode(fontNamed:"MarkerFelt-Wide")
        scoreLabelNode.position = CGPoint( x: self.frame.midX, y: 3 * self.frame.size.height / 4 )
        scoreLabelNode.zPosition = 100
        scoreLabelNode.text = String(score)
        self.addChild(scoreLabelNode)

    

    func spawnPipes() 
        let pipePair = SKNode()
        pipePair.position = CGPoint( x: self.frame.size.width + pipeTextureUp.size().width * 2, y: 0 )
        pipePair.zPosition = -10

        let height = UInt32( self.frame.size.height / 4)
        let y = Double(arc4random_uniform(height) + height)

        let pipeDown = SKSpriteNode(texture: pipeTextureDown)
        pipeDown.setScale(2.0)
        pipeDown.position = CGPoint(x: 0.0, y: y + Double(pipeDown.size.height) + verticalPipeGap)


        pipeDown.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
        pipeDown.physicsBody?.isDynamic = false
        pipeDown.physicsBody?.categoryBitMask = pipeCategory
        pipeDown.physicsBody?.contactTestBitMask = birdCategory
        pipePair.addChild(pipeDown)

        let pipeUp = SKSpriteNode(texture: pipeTextureUp)
        pipeUp.setScale(2.0)
        pipeUp.position = CGPoint(x: 0.0, y: y)

        pipeUp.physicsBody = SKPhysicsBody(rectangleOf: pipeUp.size)
        pipeUp.physicsBody?.isDynamic = false
        pipeUp.physicsBody?.categoryBitMask = pipeCategory
        pipeUp.physicsBody?.contactTestBitMask = birdCategory
        pipePair.addChild(pipeUp)

        let contactNode = SKNode()
        contactNode.name = "scoreArea"
        contactNode.position = CGPoint( x: pipeDown.size.width + bird.size.width / 2, y: self.frame.midY )
        contactNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize( width: pipeUp.size.width, height: self.frame.size.height ))
        contactNode.physicsBody?.isDynamic = false
        contactNode.physicsBody?.categoryBitMask = scoreCategory
        contactNode.physicsBody?.contactTestBitMask = birdCategory
        pipePair.addChild(contactNode)

        pipePair.run(movePipesAndRemove)
        pipes.addChild(pipePair)

    

    func resetScene ()
        // Move bird to original position and reset velocity
        bird.position = CGPoint(x: self.frame.size.width / 2.5, y: self.frame.midY)
        bird.physicsBody?.velocity = CGVector( dx: 0, dy: 0 )
        bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory
        bird.speed = 1.0
        bird.zRotation = 0.0

        // Remove all existing pipes
        pipes.removeAllChildren()
        contactState = false
        // Reset _canRestart
        canRestart = false

        // Reset score
        score = 0
        scoreLabelNode.text = String(score)

        // Restart animation
        moving.speed = 1
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) 
        if moving.speed > 0  
            for _ in touches  // do we need all touches?
                bird.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
                bird.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 120))
            
         else if canRestart 
            self.resetScene()
        
    

    override func update(_ currentTime: TimeInterval) 
        /* Called before each frame is rendered */
        let value = bird.physicsBody!.velocity.dy * ( bird.physicsBody!.velocity.dy < 0 ? 0.003 : 0.001 )
        bird.zRotation = min( max(-1, value), 0.5 )
    



    func didBegin(_ contact: SKPhysicsContact) 

        if moving.speed > 0 
            if ( contact.bodyA.categoryBitMask & scoreCategory ) == scoreCategory || ( contact.bodyB.categoryBitMask & scoreCategory ) == scoreCategory 
                // Bird has contact with score entity

                if(contact.bodyB.node != nil && contact.bodyA.node != nil)
                score += 1
                scoreLabelNode.text = String(score)

                    if (contact.bodyA.node?.name == "scoreArea") 
                        contact.bodyA.node?.removeFromParent()
                     else if contact.bodyB.node?.name == "scoreArea" 
                        contact.bodyB.node?.removeFromParent()
                    

                // Add a little visual feedback for the score increment
                scoreLabelNode.run(SKAction.sequence([SKAction.scale(to: 1.5, duration:TimeInterval(0.1)), SKAction.scale(to: 1.0, duration:TimeInterval(0.1))]))
                
             else 

                moving.speed = 0
                bird.physicsBody?.collisionBitMask = worldCategory
                bird.run(  SKAction.rotate(byAngle: CGFloat(Double.pi) * CGFloat(bird.position.y) * 0.01, duration:1), completion:self.bird.speed = 0 )


                // Flash background if contact is detected
                self.removeAction(forKey: "flash")
                self.run(SKAction.sequence([SKAction.repeat(SKAction.sequence([SKAction.run(
                    self.backgroundColor = SKColor(red: 1, green: 0, blue: 0, alpha: 1.0)
                ),SKAction.wait(forDuration: TimeInterval(0.05)), SKAction.run(
                    self.backgroundColor = self.skyColor
                ), SKAction.wait(forDuration: TimeInterval(0.05))]), count:4), SKAction.run(
                    self.canRestart = true
                )]), withKey: "flash")
            
        
    

【问题讨论】:

Why are didBeginContact called multiple times?的可能重复 【参考方案1】:

对于刚开始使用物理进行游戏开发的人们来说,这是一个常见问题。

func didBegin(_contact: SKPhysicsContact) 每秒被调用 60 次(最佳)。所以这意味着当你的“鸟”与一个物体碰撞时,它每秒会被检测到 60 次。您需要在某处放置一个标志以确定鸟是否与物体发生碰撞,因此如果标志为真,则不再进行这些碰撞检查。

鉴于您自己的类中没有鸟,您可以在 GameScene 中放置一个变量来存储此碰撞。

private var isDead = false

然后当对象与管道碰撞时将其设置为 true

isDead = true

最后在检查碰撞时使用它

if moving.speed > 0 && !isDead 

或者我更喜欢在你的 didBegin 函数的开头。这种方式可以节省您和“如果”嵌套在您的代码中

guard moving.speed > 0, !isDead else  return 

【讨论】:

问题不是因为在多个帧上调用了 didBegin 接触,问题是移动对象在同一帧上有多个接触点。此答案中的其他所有内容都是正确的(文件消失了,我们中的一些人对此问题进行了详细介绍)

以上是关于多次调用 SpriteKit 碰撞的主要内容,如果未能解决你的问题,请参考以下文章

在 SpriteKit 项目中包含一个 UIKit 小游戏

基于 SpriteKit 中的碰撞检测更新分数的问题

Spritekit 碰撞检测无法正常工作

SpriteKit物理引擎碰撞中5个重要信息

未检测到 spritekit 物理碰撞

未检测到 SpriteKit 碰撞