One-Way Platform Collisions in Sprite Kit

Sprite-Kit registering multiple collisions for single contact

OK - it would appear that a simple:

        if bomb == nil {return}

is all that's required. This should be added as follows:

        let bomb = contact.bodyA.categoryBitMask == category.bomb.rawValue ? contact.bodyA.node : contact.bodyB.node
if bomb == nil {return}

This works to prevent multiple collisions for a node that you removeFromParent in didBeginContact. If you don;t remove the node but are still registering multiple collisions, then use the node's userData property to set some sort of flag indicating that the node i s'anactive' Alternately, subclass SKSPriteNode and add a custom 'isActive' property, which is what I did to solve my problem of bullets passing up the side of an invader and taking out that invader and the one above it. This never happens on a 'direct hit'.

It doesn't answer the underlying question as to why SK is registering multiple collisions, but it does mean that I can remove all the extra code concerning setting contactTestBitMasks to 0 and then back to what they should be later etc.

Edit: So it appears that if you delete a node in didBeginContact, the node is removed but the physics body isn't. So you have to be careful as Sprite-Kit appears to build an array of physicsContacts that have occurred and then calls dBC multiple times, once for each contact. So if you are manipulating nodes and also removing them in dBC, be aware that you might run into an issue if you force unwrap a node's properties.

Trying to make platforms that I can jump through from underneath but land on top of. having trouble fine tuning the logic

Here is a link to the project that I made for macOS and iOS targets:

https://github.com/fluidityt/JumpUnderPlatform

Basically, this all has to do with

  1. Detecting collision of a platform
  2. Then determining if your player is under the platform
  3. Allow your player to go through the platform (and subsequently land on it)

--

SK Physics makes this a little complicated:


  1. On collision detection, your player's .position.y or .velocity.dy
    may already have changed to a "false" state in reference to satisfying the #2 check from above (meaning #3 will never happen). Also, your player will bounce off the platform on first contact.

  2. There is no "automatic" way to determine when your player has finished passing through the object (thus to allow player to land on the platform)

--

So to get everything working, a bit of creativity and ingenuity must be used!


1: Detecting collision of a platform:

So, to tackle 1 is the simplest: we just need to use the built in didBegin(contact:)

We are going to be relying heavily on the 3 big bitMasks, contact, category, and collision:

(fyi, I don't like using enums and bitmath for physics because I'm a rebel idiot):

struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4) // JUP = JumpUnderPlatform
}

override func didBegin(_ contact: SKPhysicsContact) {

// Crappy way to do "bit-math":
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask

switch contactedSum {

case BitMasks.jupCategory + BitMasks.playerCategory:
// ...
}

--

Now, you said that you wanted to use the SKSEditor, so I have accommodated you:

Sample Image
Sample Image
Sample Image
Sample Image

// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {

var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??

// NOTE: I could not properly configure any SKNode properties here..
// it's like they all get RESET if you put them in here...
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}

--

Now for the player:

class Player: SKSpriteNode {

// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }

static func makePlayer() -> Player {

let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)

newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true

newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.

return newPlayer
}
}


2. (and dealing with #4): Determining if under platform on contact:

There are many ways to do this, but I chose to use the player.pb.velocity.dy approach as mentioned by KOD to keep track of the player's position... if your dy is over 0, then you are jumping (under a platform) if not, then you are either standing still or falling (need to make contact with the platform and stick to it).

To accomplish this we have to get a bit more technical, because again, the physics system and the way SK works in its loop doesn't always mesh 100% with how we think it should work.

Basically, I had to make an initialDY property for Player that is constantly updated each frame in update

This initialDY will give us the correct data that we need for the first contact with the platform, allowing us to tell us to change the collision mask, and also to reset our player's CURRENT dy to the initial dy (so the player doesn't bounce off).


3. (and dealing with #5): Allow player to go through platform

To go through the platform, we need to play around with the collisionBitMasks. I chose to make the player's collision mask = the player's categoryMask, which is probably not the right way to do it, but it works for this demo.

You end up with magic like this in didBegin:

  // Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }

// Gives us the ability to pass through the platform!
player.pb.collisionBitMask = BitMasks.playerCategory

Now, dealing with #5 is going to require us to add another piece of state to our player class.. we need to temporarily store the contacted platform so we can check if the player has successfully finished passing through the platform (so we can reset the collision mask)

Then we just check in didFinishUpdate if the player's frame is above that platform, and if so, we reset the masks.

Here are all of the files , and again a link to the github:

https://github.com/fluidityt/JumpUnderPlatform



Player.swift:

class Player: SKSpriteNode {

// If you see this on a crash, then WHY DOES PLAYER NOT HAVE A PB??
var pb: SKPhysicsBody { return self.physicsBody! }

// This is set when we detect contact with a platform, but are underneath it (jumping up)
weak var platformToPassThrough: JumpUnderPlatform?

// For use inside of gamescene's didBeginContact (because current DY is altered by the time we need it)
var initialDY = CGFloat(0)
}

// MARK: - Funkys:
extension Player {
static func makePlayer() -> Player {

let newPlayer = Player(color: .blue, size: CGSize(width: 50, height: 50))
let newPB = SKPhysicsBody(rectangleOf: newPlayer.size)

newPB.categoryBitMask = BitMasks.playerCategory
newPB.usesPreciseCollisionDetection = true

newPlayer.physicsBody = newPB
newPlayer.position.y -= 200 // For demo purposes.

return newPlayer
}

func isAbovePlatform() -> Bool {
guard let platform = platformToPassThrough else { fatalError("wtf is the platform!") }

if frame.minY > platform.frame.maxY { return true }
else { return false }
}

func landOnPlatform() {
print("resetting stuff!")
platformToPassThrough = nil
pb.collisionBitMask = BitMasks.jupCategory
}
}

// MARK: - Player GameLoop:
extension Player {

func _update() {
// We have to keep track of this for proper detection of when to pass-through platform
initialDY = pb.velocity.dy
}

func _didFinishUpdate() {

// Check if we need to reset our collision mask (allow us to land on platform again)
if platformToPassThrough != nil {
if isAbovePlatform() { landOnPlatform() }
}
}
}


JumpUnderPlatform & BitMasks.swift (respectively:)

// Do all the fancy stuff you want here...
class JumpUnderPlatform: SKSpriteNode {

var pb: SKPhysicsBody { return self.physicsBody! } // If you see this on a crash, then WHY DOES JUP NOT HAVE A PB??

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }

}

struct BitMasks {
static let playerCategory = UInt32(2)
static let jupCategory = UInt32(4)
}


GameScene.swift:

-

MAKE SURE YOU HAVE THE TWO NODES IN YOUR SKS EDITOR:
Sample Image
Sample Image

-

// MARK: - Props:
class GameScene: SKScene, SKPhysicsContactDelegate {

// Because I hate crashes related to spelling errors.
let names = (jup: "jup", resetLabel: "resetLabel")

let player = Player.makePlayer()
}

// MARK: - Physics handling:
extension GameScene {

private func findJup(contact: SKPhysicsContact) -> JumpUnderPlatform? {
guard let nodeA = contact.bodyA.node, let nodeB = contact.bodyB.node else { fatalError("how did this happne!!??") }

if nodeA.name == names.jup { return (nodeA as! JumpUnderPlatform) }
else if nodeB.name == names.jup { return (nodeB as! JumpUnderPlatform) }
else { return nil }
}

// Player is 2, platform is 4:
private func doContactPlayer_X_Jup(platform: JumpUnderPlatform) {

// Check if jumping; if not, then just land on platform normally.
guard player.initialDY > 0 else { return }

// Gives us the ability to pass through the platform!
player.physicsBody!.collisionBitMask = BitMasks.playerCategory

// Will push the player through the platform (instead of bouncing off) on first hit
if player.platformToPassThrough == nil { player.pb.velocity.dy = player.initialDY }
player.platformToPassThrough = platform
}

func _didBegin(_ contact: SKPhysicsContact) {

// Crappy way to do bit-math:
let contactedSum = contact.bodyA.categoryBitMask + contact.bodyB.categoryBitMask

switch contactedSum {

case BitMasks.jupCategory + BitMasks.playerCategory:
guard let platform = findJup(contact: contact) else { fatalError("must be platform!") }
doContactPlayer_X_Jup(platform: platform)

// Put your other contact cases here...
// case BitMasks.xx + BitMasks.yy:

default: ()
}
}
}

// MARK: - Game loop:
extension GameScene {

// Scene setup:
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
addChild(player)
}

// Touch handling: (convert to touchesBegan for iOS):
override func mouseDown(with event: NSEvent) {
// Make player jump:
player.pb.applyImpulse(CGVector(dx: 0, dy: 50))

// Reset player on label click (from sks file):
if nodes(at: event.location(in: self)).first?.name == names.resetLabel {
player.position.y = frame.minY + player.size.width/2 + CGFloat(1)
}
}

override func update(_ currentTime: TimeInterval) {
player._update()
}

func didBegin(_ contact: SKPhysicsContact) {
self._didBegin(contact)
}

override func didFinishUpdate() {
player._didFinishUpdate()
}
}

I HOPE THIS HELPS SOME!

SpriteKit - Make a sprite move similar t another sprite

SKPhysicsBody has a property called node, which is the parent sprite it's attached to. In your collision method, test if the physics body is of collision category platform, then:

SKSpriteNode *thisCollidedPlatform = (SKSpriteNode*) nameOfPhysicsBody.node

And then use the position of thisCollidedPlatform to set the player position.

How to detect collisions in Sprite Kit?

You should group your physics body, you code seems to be all over the place. Also the reason you get the error is because you are settings the physics body before setting the player and missing some for the Orbs.

Here is your same code rewritten and I think a bit more readable and understandable.

import SpriteKit

/// Physics category
struct PhysicsCategory {
static let player: UInt32 = 0x1 << 0
static let orb: UInt32 = 0x1 << 1
static let floor: UInt32 = 0x1 << 2
static let platform: UInt32 = 0x1 << 3
}

/// Image names
struct ImageName {
static let platform1 = "platform2.png"
static let platform2 = "platform2.png"

// can do this for other images as well, but because you create loads of platforms its nice to have a refernce like this
}

/// Always put your keys for removing actions etc into structs to avoid typos
struct Key {
static let moveAction = "moveAction"
static let shrink = "shrink"
static let rotate = "rotate"
}

/// GameScene
class GameScene: SKScene, SKPhysicsContactDelegate {

//variables
let character = SKSpriteNode(imageNamed:"square_red.png")
let floor = SKSpriteNode(imageNamed: "platform.jpg")
var platform1 = SKSpriteNode()
var platform2 = SKSpriteNode()
let orbNode = SKSpriteNode(imageNamed: "PowerUp.png")
var characterSize:CGFloat = 0.2
var foodCount = 0

//didMoveToView
override func didMoveToView(view: SKView) {

//World Physics
self.physicsWorld.gravity = CGVectorMake(0.0, -5.0)
self.physicsWorld.contactDelegate = self
self.physicsBody = SKPhysicsBody(edgeLoopFromRect: self.frame)

//Character
character.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
character.setScale(characterSize)
character.physicsBody = SKPhysicsBody(rectangleOfSize: character.size)
character.physicsBody?.allowsRotation = false
character.physicsBody?.dynamic = true // must be true for collision to fire
character.physicsBody?.affectedByGravity = false
character.physicsBody?.categoryBitMask = PhysicsCategory.player
character.physicsBody?.contactTestBitMask = PhysicsCategory.orb
character.physicsBody?.collisionBitMask = PhysicsCategory.floor | PhysicsCategory.platform
self.addChild(character)

//floor
floor.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame)*0.2)
floor.setScale(2.0)
floor.physicsBody = SKPhysicsBody(rectangleOfSize: floor.size)
floor.physicsBody?.dynamic = false
floor.physicsBody?.affectedByGravity = false
floor.physicsBody?.categoryBitMask = PhysicsCategory.floor
///floor.physicsBody?.contactTestBitMask = not needed for now as character is set
///floor.physicsBody?.collisionBitMask = not needed for now as character is set

self.addChild(floor)

//platform one
platform1 = createPlatform(ImageName.platform1)
platform1.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame)*0.7)
platform1.setScale(0.4)
self.addChild(platform1)

//platform two
platform2 = createPlatform(ImageName.platform2)
platform2.position = CGPoint(x: CGRectGetMidX(self.frame)*1.4, y: CGRectGetMidY(self.frame)*1)
platform2.setScale(0.4)
self.addChild(platform2)

//orbNode
orbNode.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
orbNode.setScale(0.2)
orbNode.physicsBody = SKPhysicsBody(rectangleOfSize: character.size)
orbNode.physicsBody?.allowsRotation = false
orbNode.physicsBody?.dynamic = false
orbNode.physicsBody?.affectedByGravity = false
orbNode.physicsBody?.categoryBitMask = PhysicsCategory.orb
//orbNode.physicsBody?.contactTestBitMask = // not needed for now because pla
orbNode.physicsBody?.collisionBitMask = PhysicsCategory.platform //
self.addChild(orbNode)
}

/// Create platform (this way you can crate multiple platforms and save yourself tones of code)
func createPlatform(imageNamed: String) -> SKSpriteNode {
let platform = SKSpriteNode(imageNamed: imageNamed)
platform.physicsBody = SKPhysicsBody(rectangleOfSize: platform.size)
platform.physicsBody?.dynamic = false
platform.physicsBody?.affectedByGravity = false
platform.physicsBody?.categoryBitMask = PhysicsCategory.platform
//platform.physicsBody?.contactTestBitMask = /// not needed unless you want didBeginContact
platform.physicsBody?.collisionBitMask = 0 /// Not needed if you tell character to collide with it
return platform
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
character.removeActionForKey(Key.moveAction)
character.removeActionForKey(Key.shrink)
character.removeActionForKey(Key.rotate)
}

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)

if location.x < CGRectGetMidX(self.frame) && location.y < CGRectGetMidY(self.frame)*0.7{
//shrinks with each movement
characterSize-=0.005
let moveAction = SKAction.repeatActionForever(SKAction.moveByX(-30, y: 0, duration: 0.1))
let shrink = SKAction.repeatActionForever(SKAction.scaleTo(characterSize, duration: 0.1))
character.runAction(moveAction, withKey: Key.moveAction)
character.runAction(shrink, withKey: Key.shrink)
} else if location.x > CGRectGetMidX(self.frame) && location.y < CGRectGetMidY(self.frame)*0.7{
//shrinks with each movement
characterSize-=0.005
let moveAction = SKAction.repeatActionForever(SKAction.moveByX(30, y: 0, duration: 0.1))
let shrink = SKAction.repeatActionForever(SKAction.scaleTo(characterSize, duration: 0.1))
character.runAction(moveAction, withKey: Key.moveAction)
character.runAction(shrink, withKey: Key.shrink)
} else if location.y > character.position.y + 15 {
//shrinks with each movement
characterSize-=0.005
character.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 50))
let shrink = SKAction.repeatActionForever(SKAction.scaleTo(characterSize, duration: 0.1))
character.runAction(shrink, withKey: Key.shrink)
}
}
}

func didBeginContact(contact: SKPhysicsContact){

var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody

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

/// Player with Orb
if (firstBody.categoryBitMask == PhysicsCategory.player) && (secondBody.categoryBitMask == PhysicsCategory.orb) {

///Player hit orb, remove Orb
secondBody.node?.removeFromParent()
}
}

override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */

}

}

Sprite Kit Collision without bouncing

Had the same issue just a minute ago. It works with setting the restitution but you need to set both restitutions. The one of the hero AND the one of the platform (or in my case the scene boundaries).

so:

hero.physicsBody.collisionBitMask = platformCategory
hero.physicsBody.restitution = 0.0
platform.physicsBody.restitution = 0.0
any_other_object_that_should_still_bounce.physicsBody.restitution = 1.0

will do it. All other objects on the screen still bounce on the platform as long as you set their restitution > 0

Detecting collision on only one side of a rectangular physics body - Swift3

You could achieve that by detecting collisions with your rectangle and then deciding whether the collision was with the side of your interest or not. Here is a discussion about how to do that. Good luck!



Related Topics



Leave a reply



Submit