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:
- A surface - Green
- A left wall - Yellow
- A right wall - Red
I can configure turning each of these on or off with custom classes.
- The blue water blob is a deadZone that kills a player
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.
- Bug doesn't occur after first death
- Bug occurs after second death
- Bug occurs after third death
- Bug occurs after forth death
- Bug occurs after fifth death
- 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
- 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...
- After the death at the 13 seconds mark in the video
- After the death at the 16 seconds mark in the video
- 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.