swiftsprite-kitskphysicsbodyskphysicscontact

Resetting didBeginContact() ... or what the scene thinks is being contacted on Player Death?


I'm dealing with a bug I've really struggled with fixing,

My game consists of a Player sprite that interacts with parts of a platform... a standard platformer. I set up my game by attaching thin physics blocks to a larger platform physics body (big white blocks)

Each thin block represents either:

I can configure turning each of these on or off with custom classes.

enter image description here


The bug description


So I'm dealing with an interesting bug right now. The way it occurs is if my Player sprite hits a deadZone physics body (area that kills a character).

A bug occurs after the character is revived, via a revive() function. It seems didBeginContact is called and believes my character is in contact with a wall. Even though there is no wall physics body in sight.

The bug only occurs if I start pressing jump rapidly after the character is revived. Waiting a brief period before pressing jump makes the bug not occur.

When contacting the deadZone my didBeginContact() code in my GameScene class looks like this

@objc(didBeginContact:) func didBegin(_ contact: SKPhysicsContact) {


        let firstBody: SKPhysicsBody
        let secondBody: SKPhysicsBody

        if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
            firstBody = contact.bodyA
            secondBody = contact.bodyB
        } else {
            firstBody = contact.bodyB
            secondBody = contact.bodyA
        }

    //MARK:  PLAYER with DEADZOne


    if ( firstBody.categoryBitMask == BodyType.player.rawValue && secondBody.categoryBitMask == BodyType.deadZone.rawValue) {

        print("deadzone contact with Player")
        if let somePlayer:Player = firstBody.node as? Player {
            somePlayer.changeRotation(newAngle: 0)
            killPlayer(somePlayer) 
        }
    }

Note the function isn't complete... (missing things like Player - Wall Left (see below) , Player - Wall Right )

I know didBeginContact is being called due to the debug console issuing a print statement I wrote from within the Wall - Player part of the function when the bug occurs. It prints "contact with wall didBegin" , which prints when you contact either the left or right wall.

//MARK:  PLAYER with PLATFORM WALL (LEFT)

if ( firstBody.categoryBitMask == BodyType.player.rawValue && secondBody.categoryBitMask == BodyType.wallLeft.rawValue){


    if let somePlayer:Player = firstBody.node as? Player,
        let theWall:Platform = secondBody.node as? Platform
    {

        if somePlayer.jumpedOccured {

            print("contact with wall didBegin")

            somePlayer.onRightWall = false
            somePlayer.onLeftWall = true


            if somePlayer.isWallJumping {
                somePlayer.stopWallJump()
                playerOnWall(somePlayer, platformNode: theWall)

            } else {
                print("contact with wall didBegin")
                playerOnWall(somePlayer, platformNode: theWall)
            }

        }
    }
}

The way my Player moves back to the start is through code that moves him back to a respawn point, an invisible sprite at the level start, by calling killPlayer() which calls a function containing this code in the GameScene class:

           var startBackToLocation:CGPoint = CGPoint.zero

            if (somePlayer == thePlayer) {

                if ( respawnPointPlayer1 != CGPoint.zero) {
                    startBackToLocation = respawnPointPlayer1     
                }  

            let wait:SKAction = SKAction.wait(forDuration: somePlayer.reviveTime)
            let move:SKAction = SKAction.move(to: startBackToLocation, duration:0)
            let run:SKAction = SKAction.run {
            somePlayer.playerIsDead = false;
            somePlayer.revive()
        }

        let seq:SKAction = SKAction.sequence([wait, move, wait, run])
        somePlayer.run(seq)

I can prevent the bug from occurring by simply increasing the wait time e.g.

let wait:SKAction = SKAction.wait(forDuration: 1)

This however, makes for less fluid gameplay.

I'm not sure exactly what the issue is?

I can also prevent the bug from occurring if I remove all wall physics bodies from a level , to only have surface physics bodies...

My guess is that it's related to SpriteKit's rendering loop first calling didEvaluateActions... before calling didStimulatePhysics ...

So didEvaluateActions moves the Player sprite back to the start. Perhaps the player touches a wall as it's being moved back?

Perhaps didStimulatePhysics hasn't fully caught up? Maybe the loop needs to run again before it recognizes that the Player's physics body has moved?

I'm kinda lost and have been trying to fix it for a while.


Here's a video demonstrating the bug. YouTube Link


First Level in Video

The video shows the bug not occurring on a level with no wall physics bodies.


Second Level

Then the video shows it occurring on the next level, which has wall physics bodies.

  1. Bug doesn't occur after first death
  2. Bug occurs after second death
  3. Bug occurs after third death
  4. Bug occurs after forth death
  5. Bug occurs after fifth death
  6. Bug occurs after sixth death

etc


What hasn't worked


1. Setting isDynamic to false on contact with deadZone.

Then changing it to true with revive() in the Player Class

// On Contact with DeadZone this code is executed
self.physicsBody?.isDynamic = false 

// On revive()
self.physicsBody?.isDynamic = true 

2. Removing the physics body by setting it to nil on deadZone contact, then recreating it on revive(). Weirdly , during testing I can visually see the physics rectangle removed on deadZone contact...and then reappear... it still doesn't solve the problem.

somePlayer.physicsBody = nil

3. Trying to reset Player's contactTestMask.. By setting it to zero on deadZone contact, then resetting it to its defaults on revive().

e.g. on DeadZone contact

somePlayer.physicsBody?.contactTestBitMask = 0

e.g. on revive()

 self.physicsBody?.contactTestBitMask = BodyType.platformSurface.rawValue | BodyType.platformCeiling.rawValue | BodyType.wallRight.rawValue |  BodyType.wallLeft.rawValue | BodyType.pole.rawValue |  BodyType.portal.rawValue |  BodyType.deadZone.rawValue |  BodyType.coin.rawValue | BodyType.player.rawValue | BodyType.zipLine.rawValue | BodyType.springBoard.rawValue

4. Using a return statement for each Player - ___ contact to exit scope if playerIsDead.

e.g. on Left Wall contact

if somePlayer.playerIsDead == true {return} 

5. Directly setting the player position instead of using an SKAction.move on Player death.

let wait:SKAction = SKAction.wait(forDuration: somePlayer.reviveTime)
let move:SKAction = SKAction.run {
    somePlayer.position = startBackToLocation
}

let run:SKAction = SKAction.run {
    somePlayer.playerIsDead = false;
    somePlayer.revive()
}

let seq:SKAction = SKAction.sequence([wait, move, wait, run])
somePlayer.run(seq)

What reduces the frequency of the bug


  1. I attempted a "reset physics function" in the Player Class ... called from my revive() function in the Player Class.

while still setting somePlayer.physicsBody = nil on DeadZone contact

func resetPlayerPhysicsBody() {

    let removePhysicsBody: SKAction = SKAction.run {
        self.physicsBody = nil
        self.canJumpAgain = false
    }


    let wait: SKAction = SKAction.wait(forDuration: self.reviveTime)

    let resetToDefaults: SKAction = SKAction.run {
        self.setUpPlayerPhysics() // Recreates initial physics body //
        self.canJumpAgain = true
    }

    let seq:SKAction = SKAction.sequence([removePhysicsBody, wait, resetToDefaults])
    self.run(seq)}

This actually makes the error... occur at a much lower frequency... it almost eliminates it... but it also has some strange side effects , like the player often shifting right a bit after spawning.

Though with these additions of code it only occurs 3X in my lastest video...

  1. After the death at the 13 seconds mark in the video
  2. After the death at the 16 seconds mark in the video
  3. After the death at the 1 minute and 13 seconds mark in the video

Here's a video of this attempt in action. YouTube Link


Anyone have any ideas on a fix?

I'm guessing some way to "reset" or "flush" what SpriteKit thinks is being contacted... but how and why this occurring also bugs me.


Solution

  • So I finally found a fix... It's not super elegant.

    If anyone has better solutions. Please do post them though, I'd love a less messy solution and perhaps something that makes more sense as this solution imposes some limits to how I can design things.


    1. I used the following new variables in my GameScene class

    class GameScene: SKScene, SKPhysicsContactDelegate {
        var contactDetectionEnabled = true
        var wallContactDetectionEnabled = true
        var wallPhysicsOnNodePosition:CGPoint?
    

    1. In certain levels where the bug was extra stubborn I created a "WallPhysicsOnNode", whose that triggers wall physics turning back on when the player passes it. I placed them close to the first instance where wall jumping was feasible in the game.

    If it exists, wallPhysicsOnNodePosition is assigned a value, otherwise it remains nil.


    enter image description here

    I assign a value to wallPhysicsOnNodePosition in my didMove(to:)

     self.enumerateChildNodes(withName: "//*") {
                node, stop in
    
             if (node.name == "WallPhysicsOnNode") {
                    self.wallPhysicsOnNodePosition = node.position
                    node.isHidden = true
                    print("assigning wallPhysicsOnNodePosition")
             }
     }
    

    1. I used the following new variables in my Player class

    class Player: SKSpriteNode {
        var jumpedOccured = false  // turned on via update() while isJumping //
        var fallingOccured = false // turned on via update() while isFalling //
    

    1. in didBeginContact & didEndContact in GameScene... I used the following if statement to prevent the use of the function in certain circumstances.

    @objc(didBeginContact:) func didBegin(_ contact: SKPhysicsContact) {
       if contactDetectionEnabled {
    

    @objc(didEndContact:) func didEnd(_ contact: SKPhysicsContact) {
    
        if contactDetectionEnabled {
    

    1. in didBeginContact & didEndContact in GameScene... I surrounded the Wall - Player contact parts of the code with another if statement to prevent it being being used sometimes.

     if wallContactDetectionEnabled {
    

    1. in didBeginContact in GameScene... On Player death / contact with a deadZone I used a defer statement so as the scope is exited the following occurs.

    //MARK:  PLAYER with DEADZOne
    
    
    if ( firstBody.categoryBitMask == BodyType.player.rawValue && secondBody.categoryBitMask == BodyType.deadZone.rawValue){
    
        print("deadzone contact with Player")
        if let somePlayer:Player = firstBody.node as? Player{
    
            if somePlayer.playerIsDead {
                return
            }
    
            defer {
                self.contactDetectionEnabled = false
                self.wallContactDetectionEnabled = false
                killPlayer(somePlayer)
            }     
        }
    }
    

    1. My GameScene has the following in it's update(). Which makes the wall physics only turn on after the player passes that node... which is placed at the point where wall jumping first can occur.

       override func update(_ currentTime: TimeInterval) {
           /* Called before each frame is rendered */
    
           if (transitionInProgress == false) {
             thePlayer.update()
             thePlayer2.update()
    
                defer {
                    if thePlayer.jumpedOccured && !thePlayer.playerIsDead && thePlayer.touchOccured && thePlayer.fallingOccured && !contactDetectionEnabled {
                            contactDetectionEnabled = true
                    }
    
                    if thePlayer.jumpedOccured && !thePlayer.playerIsDead && thePlayer.touchOccured && thePlayer.fallingOccured && !wallContactDetectionEnabled {
                        if let wallOnNodePosUnwrapped = wallPhysicsOnNodePosition {
                            if thePlayer.position.x >= wallOnNodePosUnwrapped.x {
                                wallContactDetectionEnabled = true
                                print("Player passed wallPhysicsOnNodePosition ... so  \(wallContactDetectionEnabled)")
                            }
                        } else if wallPhysicsOnNodePosition == nil {
                            wallContactDetectionEnabled = true
                            print("No wallPhysicsOnNodePosition ... but wallContactDetectionEnabled is \(wallContactDetectionEnabled)")
                        }
                    }
                }
    

    There's a little more to it... but you can pretty much figure it out...

    The use of Swifts defer { } ability really was the key to my solution.

    The use of the wallContactDetectionEnabled was almost not necessary ... but I still got the bug occasionally so I added that part and the use of the wallPhysicsOnNodePosition optional to control it in cases where the bug still seemed to persist.

    But, for all levels I'll add a WallPhysicsOnNode at the point where wall jumping can first occur.


    Again the solution is far from what would be ideal... it's a little messy and I may clean it up later... but it works just fine.

    Wish there was something better... but I tried many things that logically should have worked and didn't. Adding the code directly above shouldn't have been logically necessary. I was shocked I needed the additional if statement that encapsulated the wall - player contact . The bug hardly occurred without it... but for some reason it occasionally did.

    Wish there was something more elegant... as this is a real "duct tape solution". But, at least it works.

    If anyone has any comments, feel free to share them. My answer may work... but it only prevents the situation from occurring. It doesn't technically get to the bottom of what was going on.

    That may cause problems in the future.