I'm facing a problem with collision detection.
Current setup
1) I extended ViewController class with SCNPhysicsContactDelegate:
class ViewController: UIViewController, SCNPhysicsContactDelegate
2) I setup an OptionSet to manage collision categories:
// Collisions
struct CollisionCategory: OptionSet {
let rawValue: Int
static let CoinsCategory = CollisionCategory(rawValue: 0) // Coin SCNNode
static let CarCategory = CollisionCategory(rawValue : 1) // Car SCNNode
static let FinishLineCategory = CollisionCategory(rawValue: 2) // FinishLine SCNNode
}
3) I setup categoryBitMask, contactTestBitMask and collisionBitMask of all of them:
// Setup car
carNode.physicsBody?.categoryBitMask = CollisionCategory.CarCategory.rawValue
carNode.physicsBody?.contactTestBitMask = CollisionCategory.CoinsCategory.rawValue | CollisionCategory.FinishLineCategory.rawValue
carNode.physicsBody?.collisionBitMask = CollisionCategory.CoinsCategory.rawValue | CollisionCategory.FinishLineCategory.rawValue
// Setup finishLine
planeNode.physicsBody?.categoryBitMask = CollisionCategory.FinishLineCategory.rawValue
planeNode.physicsBody?.contactTestBitMask = CollisionCategory.CarCategory.rawValue
planeNode.physicsBody?.collisionBitMask = CollisionCategory.CarCategory.rawValue
// Setup coin
coin.physicsBody?.categoryBitMask = CollisionCategory.CoinsCategory.rawValue
coin.physicsBody?.contactTestBitMask = CollisionCategory.CarCategory.rawValue
coin.physicsBody?.collisionBitMask = CollisionCategory.CarCategory.rawValue
4) I setup physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) to manage collision when they happen:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
if(contact.penetrationDistance >= 0.08 && contact.nodeB.name!.contains("Coin")) {
// Remove coin from parent
contact.nodeB.removeFromParentNode()
// Increment counter
self.coinsCounter += 1
DispatchQueue.main.async {
// Increase coin counter
let view = self.arSceneView.viewWithTag(10) as! UILabel
view.text = String(self.coinsCounter)
}
}
// PROBLEMS HERE
if(contact.penetrationDistance >= 0.076 && contact.nodeB.name!.contains("FinishLine") && !self.startFinishLineCollision && !self.outOfTrack) {
self.startFinishLineCollision = true
// Increment lap
if(seconds > 10) {
self.laps += 1
DispatchQueue.main.async {
(self.arSceneView.viewWithTag(14) as! UILabel).text = String(self.laps)
}
if(self.laps == 4){
endGame()
}
}
}
}
Problem
I need to check when car touches finish line to mark a lap and increment the lap counter by 1. The problem is that I'm getting continuously contact messages from this function between nodeA and nodeB, even if there is no collision between car and finish line.
For example, in the following image there is a collision message, but there isn't an effective collision between car and finish line.
What is wrong with the current setup?
What I need
I need to check if there is a collision between car and finishline with precision.
Thank you
Update
I tried the following setup too, with no solution...
let CoinsCategory = 2
let CarCategory = 4
let FinishLineCategory = 6
carNode.physicsBody?.categoryBitMask = CarCategory
carNode.physicsBody?.collisionBitMask = CoinsCategory | FinishLineCategory
planeNode.physicsBody?.categoryBitMask = FinishLineCategory
planeNode.physicsBody?.collisionBitMask = CarCategory
coin.physicsBody?.categoryBitMask = CoinsCategory
coin.physicsBody?.collisionBitMask = CarCategory
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
switch contact.nodeB.physicsBody!.collisionBitMask {
case FinishLineCategory:
print("Finish Line Collision")
case CoinsCategory:
print("Coin Collision")
default:
print("Other collision")
}
}
No prints are called... It's like there is no collision between objects.
NOTE: finishline, car and coins are all children on the the same parent (the plane below them). Is it relevant?
Ok, I found a workaround to workaround my issue:
1) I used a coin (yellow cube) as collider.
2) I setup it with Color.clear (transparent color) in such a way as to make it invisible.
3) I placed it the center of the finish line.
4) Since contactTest is not working, I used penetrationDistance:
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
if(contact.penetrationDistance >= 0.08 && contact.nodeB.name! == "Coin") {
// Remove coin from parent
contact.nodeB.removeFromParentNode()
// Increment counter
self.coinsCounter += 1
DispatchQueue.main.async {
// Increase coin counter
let view = self.arSceneView.viewWithTag(10) as! UILabel
view.text = String(self.coinsCounter)
}
}
// >>>>> NEW CODE HERE! <<<<<
if(contact.penetrationDistance > 0.078 && contact.nodeB.name! == "CoinFinishLine" && !self.outOfTrack && !self.lapIncrement) {
self.lapIncrement = true
// Start lap increment timer (5 seconds)
self.startNSecondsTimer(howMuchTime: 1.0, target: 1)
// Increment lap
if(seconds > 10) {
self.laps += 1
DispatchQueue.main.async {
(self.arSceneView.viewWithTag(14) as! UILabel).text = String(self.laps)
}
if(self.laps == 3){
endGame()
}
}
}
}
Anyhow, if someone can explain me why I can't get contactTestBitMask to work and I can use only collisionBitMask with this solution is welcome. Thank you
Related
Currently I have a Swift/Spritekit app that drops a sprite from the sky, they have collisions off so they can fall through the floor, however I am trying to make a statement that will detect when the player sprite touches the sprite falling from the sky then deletes that child and adds a point to the score var.
Update: 04/29/18 2:10 CST
As of now this is what I have
import SpriteKit
import GameplayKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var activePlayer:SKSpriteNode! = SKSpriteNode() //Sets active character
var bg:SKSpriteNode! = SKSpriteNode()
var bananaCollected = 0 //Defines banana var
var timer: Timer?
let bananaCat : UInt32 = 0x1 << 1
let playerCat : UInt32 = 0x1 << 2
func bananaDrop() { //Defines bananaDrop
let banana = SKSpriteNode(imageNamed:"banana")
//print("- Debug: [bananaDrop] successfully initiated -")
banana.name = "banana"
//Size of banana
banana.xScale = 0.25
banana.yScale = 0.25
//Defines physics properties
let physicsBody = SKPhysicsBody(circleOfRadius: 15)
//let physicsBody = SKPhysicsBody()
//physicsBody.pinned = true //Suspend in air
physicsBody.allowsRotation = false
physicsBody.affectedByGravity = true
//physicsBody.collisionBitMask = 0
banana.physicsBody?.categoryBitMask = bananaCat
banana.physicsBody?.collisionBitMask = 0
banana.physicsBody?.contactTestBitMask = playerCat
//categoryBitMask is what the physics category of the object is
//banana.physicsBody!.categoryBitMask = playerCategory
//collisionBitMask is what the physics category of objects that this cannot pass through are...multiple categories would be typed like... `cat1 | cat2`
//banana.physicsBody!.collisionBitMask = 0
//contactTestBitMask is what the physics category of the objects that we get alerted to upon contact
//activePlayer.physicsBody!.contactTestBitMask = obstacleCategory
banana.physicsBody = physicsBody
//Starting Location Defined
var x: CGFloat = 0 //Defines X
let y: CGFloat = 400 //Defines how high up banana drops
let bananaDrop = GKShuffledDistribution(lowestValue: 1, highestValue: 11)
//Drop locations defined (relation to X)
switch bananaDrop.nextInt() {
case 1:
x = -170
case 2:
x = -160
case 3:
x = -120
case 4:
x = -80
case 5:
x = -40
case 6:
x = 0
case 7:
x = 40
case 8:
x = 80
case 9:
x = 120
case 10:
x = 160
case 11:
x = 170
default:
fatalError("Case num outside range")
}
banana.position = CGPoint(x: x, y: y)
//Adds banana
self.addChild(banana)
}
func bDropF() { //Defintes Banana Drop Final
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { [weak self] timer in
self?.timer = timer
self?.timerTime()
})
}
//Stop droping bananas
deinit {
self.timer?.invalidate()
}
func timerTime() {
bananaDrop()
}
override func didMove(to view: SKView) {
print("- Debug: Game Scene Loaded -")
bDropF() //Calls banana drop
if let setupBG:SKSpriteNode = self.childNode(withName: "bg") as? SKSpriteNode {
bg = setupBG
bg.name = "bg"
bg.physicsBody?.affectedByGravity = false
bg.zPosition = -1
}
func didBeginContact(_ contact: SKPhysicsContact){ //Banana Collect
if let firstNode = contact.bodyA.node as? SKSpriteNode, let secondNode = contact.bodyB.node as? SKSpriteNode {
let object1: String = firstNode.name!
let object2: String = secondNode.name!
if (object1 == "player") || (object2 == "banana") {
print("colliding!")
}
}
}
//Player Definitions
if let randoPlayer:SKSpriteNode = self.childNode(withName: "player") as? SKSpriteNode { //Test for Char type
activePlayer = randoPlayer //Char Set
activePlayer.physicsBody?.isDynamic = true //Set dynamic
activePlayer.physicsBody?.affectedByGravity = true //Set dynamic gravity
activePlayer.name = "player"
print("Player Initiated")
print("Physics set :: Dynamic(true):AffectedByGravity(true)")
} else {
print("Failed to initiate player")
}
}
func moveActivePlayerR() {//Right Touch Player Movements Defined
let walkAnimation:SKAction = SKAction(named: "WalkRight")!
let moveAction:SKAction = SKAction.moveBy(x: 100, y: 0, duration: 0.5) //Move Right Side
//let moveRight:SKAction = SKAction.group([walkAnimation, moveAction]) //Depricated
let sound = SKAction.playSoundFileNamed("walk.wav", waitForCompletion: false)
let finalWalkR:SKAction = SKAction.group([walkAnimation, moveAction, sound])
activePlayer.run(finalWalkR)
}
func moveActivePlayerL() { //Left Touch Player Movements Defined
let walkAnimation:SKAction = SKAction(named: "WalkLeft")!
let moveAction:SKAction = SKAction.moveBy(x: -100, y: 0, duration: 0.5) //Move Left Side
//let moveRight:SKAction = SKAction.group([walkAnimation, moveAction]) //Depricated
let sound = SKAction.playSoundFileNamed("walk.wav", waitForCompletion: false)
let finalWalkL:SKAction = SKAction.group([moveAction, sound, walkAnimation])
activePlayer.run(finalWalkL)
}
func touchDown(atPoint pos : CGPoint) {
print("touch \( pos.x),\(pos.y)") //Debug print
if(pos.x > 0) { //if touched right side of screen
print("Right touch")
moveActivePlayerR()
} else if (pos.x < 0) { //if touched left side of screen
print("Left touch")
moveActivePlayerL()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
self.touchDown(atPoint: t.location(in: self))
}
}
}
However even with these categories setup it still appears that I am unable to get them to collide properly. When they touch each other there is nothing in my console indicating that they have collided. Please help!
You do not have to have a collisionBitMask set in order to detect contact, you just need to set the contactTestBitMask to the category of the obstacle you want to detect the collision with. You then check for the collision in the didBegin func. Ensure that you have the SKPhysicsContactDelegate set on your scene.
class GameScene: SKScene, SKPhysicsContactDelegate
self.physicsWorld.contactDelegate = self
when setting up your objects
//categoryBitMask is what the physics category of the object is
object.physicsBody!.categoryBitMask = playerCategory
//collisionBitMask is what the physics category of objects that this cannot pass through are...multiple categories would be typed like... `cat1 | cat2`
object.physicsBody!.collisionBitMask = 0
//contactTestBitMask is what the physics category of the objects that we get alerted to upon contact
object.physicsBody!.contactTestBitMask = obstacleCategory
the didBegin func in your scene
func didBeginContact(_ contact: SKPhysicsContact) {
if let firstNode = contact.bodyA.node as? SKSpriteNode, let secondNode = contact.bodyB.node as? SKSpriteNode {
let object1: String = firstNode.name!
let object2: String = secondNode.name!
if (object1 == "obstacle") || (object2 == "obstacle") {
//run some code because these 2 have collided
}
}
}
...or...
func didBeginContact(_ contact: SKPhysicsContact) {
if contact.bodyA.categoryBitMask == PhysicsCategory.obstacleCategory || contact.bodyB.categoryBitMask == PhysicsCategory.obstacleCategory {
//obstacle has hit player do something
}
}
I'm still a beginner in Swift and I'm having some trouble with collision detections in SpriteKit. I used this question from StackOverFlow which was great in showing my how to construct things neatly. But I'm having problems with my didBegin function, which does not even get called to at all. I'm hoping I missed something simple out that you guys can take a look at for me.
Thanks in advance.
Here is my PhysicsCatagoies struct:
import Foundation
import SpriteKit
struct PhysicsCatagories: OptionSet {
let rawValue: UInt32
init(rawValue: UInt32) { self.rawValue = rawValue }
static let None = PhysicsCatagories(rawValue: 0b00000) // Binary for 0
static let Player = PhysicsCatagories(rawValue: 0b00001) // Binary for 1
static let EnemyBullet = PhysicsCatagories(rawValue: 0b00010) // Binary for 2
static let PlayerBullet = PhysicsCatagories(rawValue: 0b00100) // Binary for 4
static let Enemy = PhysicsCatagories(rawValue: 0b01000) // Binary for 8
static let Boss = PhysicsCatagories(rawValue: 0b10000) // Binary for 16
}
extension SKPhysicsBody {
var category: PhysicsCatagories {
get {
return PhysicsCatagories(rawValue: self.categoryBitMask)
}
set(newValue) {
self.categoryBitMask = newValue.rawValue
}
}
}
And here is how I assigned my nodes in GameScene:
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody!.affectedByGravity = false
player.physicsBody!.categoryBitMask = PhysicsCatagories.Player.rawValue
player.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
player.physicsBody!.category = [.Enemy, .EnemyBullet, .Boss]
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
bullet.physicsBody!.affectedByGravity = false
bullet.physicsBody!.categoryBitMask = PhysicsCatagories.PlayerBullet.rawValue
bullet.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
bullet.physicsBody!.category = [.Enemy, .Boss]
enemy.physicsBody = SKPhysicsBody(rectangleOf: enemy.size)
enemy.physicsBody!.affectedByGravity = false
enemy.physicsBody!.categoryBitMask = PhysicsCatagories.Enemy.rawValue
enemy.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
enemy.physicsBody!.category = [.Player, .PlayerBullet]
enemyBullet.physicsBody = SKPhysicsBody(rectangleOf: enemyBullet.size)
enemyBullet.physicsBody!.affectedByGravity = false
enemyBullet.physicsBody!.categoryBitMask = PhysicsCatagories.EnemyBullet.rawValue
enemyBullet.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
enemyBullet.physicsBody!.category = [.Player]
boss.physicsBody = SKPhysicsBody(rectangleOf: boss.size)
boss.physicsBody!.affectedByGravity = false
boss.physicsBody!.categoryBitMask = PhysicsCatagories.Boss.rawValue
boss.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
boss.physicsBody!.category = [.Player, .PlayerBullet]
bulletSpecial.physicsBody = SKPhysicsBody(rectangleOf: bulletSpecial.size)
bulletSpecial.physicsBody!.affectedByGravity = false
bulletSpecial.physicsBody!.categoryBitMask = PhysicsCatagories.PlayerBullet.rawValue
bulletSpecial.physicsBody!.collisionBitMask = PhysicsCatagories.None.rawValue
bulletSpecial.physicsBody!.category = [.Enemy, .Boss]
Finally, this is my didBegin function, which does not seem to work at all:
func didBegin(_ contact: SKPhysicsContact) {
let contactCategory: PhysicsCatagories = [contact.bodyA.category, contact.bodyB.category]
switch contactCategory {
case [.Player, .Enemy]:
print("player has hit enemy")
case [.PlayerBullet, .Enemy]:
print("player bullet has hit enemy")
case [.PlayerBullet, .Boss]:
print("player bullet has hit boss")
case [.Player, .Boss]:
print("player has hit boss")
case [.Player, .EnemyBullet]:
print("player has hit enemy bullet")
default:
preconditionFailure("Unexpected collision type: \(contactCategory)")
}
}
I've not used the OptionSet technique for cagegoryBitMasks, so here's how I'd do it:
Define unique categories, ensure your class is a SKPhysicsContactDelegate and make yourself the physics contact delegate:
//Physics categories
let PlayerCategory: UInt32 = 1 << 0 // b'00001'
let EnemyBulletCategory: UInt32 = 1 << 1 // b'00010'
let PlayerBulletCategory: UInt32 = 1 << 2 // b'00100'
let EnemyCategory: UInt32 = 1 << 3 // b'01000'
let BossCategory: UInt32 = 1 << 4 // b'10000'
class GameScene: SKScene, SKPhysicsContactDelegate {
physicsWorld.contactDelegate = self
Assign the categories (usually in didMove(to view:) :
player.physicsBody.catgeoryBitMask = PlayerCategory
bullet.physicsBody.catgeoryBitMask = BulletCategory
enemy.physicsBody.catgeoryBitMask = EnemyCategory
enemyBullet.physicsBody.catgeoryBitMask = EnemyBulletCategory
boss.physicsBody.catgeoryBitMask = BossCategory
(not sure about bulletSpecial - looks the same as bullet)
Set up contact detection:
player.physicsBody?.contactTestBitMask = EnemyCategory | EnemyBulletCategory | BossCategory
bullet.physicsBody?.contactTestBitMask = EnemyCategory | BossCategory
enemy.physicsBody?.contactTestBitMask = PlayerCategory | PlayerBulletCategory
enemyBullet.physicsBody?.contactTestBitMask = PlayerCategory
boss.physicsBody?.contactTestBitMask = PlayerCategory | PlayerBulletCategory
Turn off collisions: (on by default)
player.physicsBody?.collisionBitMask = 0
bullet.physicsBody?.collisionBitMask = 0
enemy.physicsBody?.collisionBitMask = 0
enemyBullet.physicsBody?.collisionBitMask = 0
boss.physicsBody?.collisionBitMask = 0
Implement didBegin:
func didBegin(_ contact: SKPhysicsContact) {
print("didBeginContact entered for \(String(describing: contact.bodyA.node!.name)) and \(String(describing: contact.bodyB.node!.name))")
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch contactMask {
case PlayerCategory | EnemyCategory:
print("player has hit enemy")
case PlayerBulletCategory | EnemyCategory:
print("player bullet has hit enemy")
case PlayerBulletCategory | BossCategory:
print("player bullet has hit boss")
case PlayerCategory | BossCategory:
print("player has hit boss")
case PlayerCategory | EnemyBulletCategory:
print("player has hit enemy bullet")
default:
print("Undetected collision occurred")
}
}
It's a bit late here, so hopefully I haven't made any stupid mistakes.
=======================
You could also include this function and then call it via checkPhysics() once you have set up all your physics bodies and collisions and contact bit masks. It will go through every node and print out what collides with what and what contacts what (it doesn't check the isDynamic property, so watch out for that):
//MARK: - Analyse the collision/contact set up.
func checkPhysics() {
// Create an array of all the nodes with physicsBodies
var physicsNodes = [SKNode]()
//Get all physics bodies
enumerateChildNodes(withName: "//.") { node, _ in
if let _ = node.physicsBody {
physicsNodes.append(node)
} else {
print("\(String(describing: node.name)) does not have a physics body so cannot collide or be involved in contacts.")
}
}
//For each node, check it's category against every other node's collion and contctTest bit mask
for node in physicsNodes {
let category = node.physicsBody!.categoryBitMask
// Identify the node by its category if the name is blank
let name = node.name != nil ? node.name : "Category \(category)"
if category == UInt32.max {print("Category for \(String(describing: name)) does not appear to be set correctly as \(category)")}
let collisionMask = node.physicsBody!.collisionBitMask
let contactMask = node.physicsBody!.contactTestBitMask
// If all bits of the collisonmask set, just say it collides with everything.
if collisionMask == UInt32.max {
print("\(name) collides with everything")
}
for otherNode in physicsNodes {
if (node != otherNode) && (node.physicsBody?.isDynamic == true) {
let otherCategory = otherNode.physicsBody!.categoryBitMask
// Identify the node by its category if the name is blank
let otherName = otherNode.name != nil ? otherNode.name : "Category \(otherCategory)"
// If the collisonmask and category match, they will collide
if ((collisionMask & otherCategory) != 0) && (collisionMask != UInt32.max) {
print("\(name) collides with \(String(describing: otherName))")
}
// If the contactMAsk and category match, they will contact
if (contactMask & otherCategory) != 0 {print("\(name) notifies when contacting \(String(describing: otherName))")}
}
}
}
}
It will produce output like:
Optional("shape_blueSquare") collides with Optional("Screen_edge")
Optional("shape_redCircle") collides with Optional("Screen_edge")
Optional("shape_redCircle") collides with Optional("shape_blueSquare")
Optional("shape_redCircle") notifies when contacting Optional("shape_purpleSquare")
Optional("shape_redCircle") collides with Optional("shape_greenRect")
Optional("shape_redCircle") notifies when contacting Optional("shape_greenRect")
Optional("shape_purpleSquare") collides with Optional("Screen_edge")
Optional("shape_purpleSquare") collides with Optional("shape_greenRect")
Category for Optional("shape_greenRect") does not appear to be set correctly as 4294967295
Optional("shape_greenRect") collides with Optional("Screen_edge")
Optional("shape_yellowTriangle") notifies when contacting Optional("shape_redCircle")
Optional("shape_yellowTriangle") collides with Optional("shape_greenRect")
Optional("shape_yellowTriangle") notifies when contacting Optional("shape_greenRect")
etc.
My goal is to set up all my platforms in the .sks file for easier design of my levels.
this is declared at the top of game scene.swift before didMove:
private var JumpThroughPlatformObject = SKSpriteNode()
and this is in DidMove:
if let JumpThroughPlatformObjectNode = self.childNode(withName: "//jumpThroughPlatform1") as? SKSpriteNode {
JumpThroughPlatformObject = JumpThroughPlatformObjectNode}
I reference the platform to get it's height from the .sks, since all my platforms are going to be the same height I only need to get it from one.
Below is what Im trying to use in my update method to turn off collisions until my player is totally above the platform. The main issue with only checking if my players velocity is greater than zero is: if the player is at the peak of a jump (his velocity slows to zero). if this happens and the player is inside a platform, he either instantly springs up to the top of the platform or gets launched downward.
I don't want my platforms to have to be 1 pixel high lines. I also need to have the player have a full collision box since he will be interacting with other types of environments. This leads me to believe that I somehow need to only register the top of the platform as a collision box and not the entire platform.
This if statement I wrote is supposed to take the y position of a platform and add half of its height to it, since the y position is based on the center of the sprite I figured this would put the collision for the platform on its top boundary.
I did the same for the player but in reverse. Putting the players collisions on only the bottom of his border. But its not working perfectly and I'm not sure why at this point.
if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2))
The function below is giving me 3 main issues:
My players jump is always dy = 80. If I'm jumping up to a platform that position.y = 90, the players peak of the jump stops in the middle of the platform, but he teleports to the top of it instead of continuing to fall to the ground.
the left and right edges of the platforms still have full collision with the player if I'm falling
if my player is on a platform and there is another one directly above me, the player can't jump through it.
let zero:CGFloat = 0
if let body = player.physicsBody {
let dy = player.physicsBody?.velocity.dy
// when I jump dy is greater than zero else I'm falling
if (dy! >= zero) {
if (JumpThroughPlatformObject.position.y + (JumpThroughPlatformObject.size.height / 2)) > (player.position.y - (player.size.height / 2)) {
print(" platform y: \(JumpThroughPlatformObject.position.y)")
print ("player position: \(player.position.y)")
// Prevent collisions if the hero is jumping
body.collisionBitMask = CollisionTypes.saw.rawValue | CollisionTypes.ground.rawValue
}
}
else {
// Allow collisions if the hero is falling
body.collisionBitMask = CollisionTypes.platform.rawValue | CollisionTypes.ground.rawValue | CollisionTypes.saw.rawValue
}
}
Any advice would be greatly appreciated. I've been tearing my hair out for a couple days now.
EDIT in didBegin and didEnd:
func didBegin(_ contact: SKPhysicsContact) {
if let body = player.physicsBody {
let dy = player.physicsBody?.velocity.dy
let platform = JumpThroughPlatformObject
let zero:CGFloat = 0
if contact.bodyA.node == player {
// playerCollided(with: contact.bodyB.node!)
if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue
}
} else if contact.bodyB.node == player {
// playerCollided(with: contact.bodyA.node!)
isPlayerOnGround = true
if (dy! > zero || body.node!.intersects(platform)) && ((body.node?.position.y)! - player.size.height / 2 < platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue}
}
}
}
func didEnd(_ contact: SKPhysicsContact) {
if let body = player.physicsBody {
// let dy = player.physicsBody?.velocity.dy
// let platform = JumpThroughPlatformObject
if contact.bodyA.node == player {
body.collisionBitMask |= CollisionTypes.platform.rawValue
}else if contact.bodyB.node == player {
body.collisionBitMask |= CollisionTypes.platform.rawValue
}
}
}
Adding what I did, the player can no longer jump through the platform.
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
Detecting collision of a platform
Then determining if your player is under the platform
Allow your player to go through the platform (and subsequently land on it)
--
SK Physics makes this a little complicated:
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.
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:
// 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:
-
// 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!
You just need a condition that let's you know if you are in a body. I also cleaned up your code to avoid accidently putting in the wrong categories
if let body = player.physicsBody, let dy = body.velocity.dy {
// when I am jumping or I am in a platform, then do not register
if (dy > zero || body.node.intersects(platform) && (body.node.position.y - body.node.size.height/2 != platform.position.y + platform.size.height / 2) {
body.collisionBitMask &= ~CollisionTypes.platform.rawValue
}
else {
// Allow collisions if the hero is falling
body.collisionBitMask |= CollisionTypes.platform.rawValue
Well, The answers above work well but those are very completed.
Simple answer is use Platform effector 2D component. which applies various “platform” behavior such as one-way collisions, removal of side-friction/bounce etc.
Check out this Unity's official tutorial for more clearance.
I'm using the scene editor in SpriteKit to place color sprites and assign them textures using the Attributes Inspector. My problem is trying to figure out how to reference those sprites from my GameScene file. For example, I'd like to know when a sprite is a certain distance from my main character.
Edit - code added
I'm adding the code because for some reason, appzYourLife's answer worked great in a simple test project, but not in my code. I was able to use Ron Myschuk's answer which I also included in the code below for reference. (Though, as I look at it now I think the array of tuples was overkill on my part.) As you can see, I have a Satellite class with some simple animations. There's a LevelManager class that replaces the nodes from the scene editor with the correct objects. And finally, everything gets added to the world node in GameScene.swift.
Satellite Class
func spawn(parentNode:SKNode, position: CGPoint, size: CGSize = CGSize(width: 50, height: 50)) {
parentNode.addChild(self)
createAnimations()
self.size = size
self.position = position
self.name = "satellite"
self.runAction(satAnimation)
self.physicsBody = SKPhysicsBody(circleOfRadius: size.width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = PhysicsCategory.satellite.rawValue
self.physicsBody?.contactTestBitMask = PhysicsCategory.laser.rawValue
self.physicsBody?.collisionBitMask = 0
}
func createAnimations() {
let flyFrames:[SKTexture] = [textureAtlas.textureNamed("sat1.png"),
textureAtlas.textureNamed("sat2.png")]
let flyAction = SKAction.animateWithTextures(flyFrames, timePerFrame: 0.14)
satAnimation = SKAction.repeatActionForever(flyAction)
let warningFrames:[SKTexture] = [textureAtlas.textureNamed("sat8.png"),
textureAtlas.textureNamed("sat1.png")]
let warningAction = SKAction.animateWithTextures(warningFrames, timePerFrame: 0.14)
warningAnimation = SKAction.repeatActionForever(warningAction)
}
func warning() {
self.runAction(warningAnimation)
}
Level Manager Class
import SpriteKit
class LevelManager
{
let levelNames:[String] = ["Level1"]
var levels:[SKNode] = []
init()
{
for levelFileName in levelNames {
let level = SKNode()
if let levelScene = SKScene(fileNamed: levelFileName) {
for node in levelScene.children {
switch node.name! {
case "satellite":
let satellite = Satellite()
satellite.spawn(level, position: node.position)
default: print("Name error: \(node.name)")
}
}
}
levels.append(level)
}
}
func addLevelsToWorld(world: SKNode)
{
for index in 0...levels.count - 1 {
levels[index].position = CGPoint(x: -2000, y: index * 1000)
world.addChild(levels[index])
}
}
}
GameScene.swift - didMoveToView
world = SKNode()
world.name = "world"
addChild(world)
physicsWorld.contactDelegate = self
levelManager.addLevelsToWorld(self.world)
levelManager.levels[0].position = CGPoint(x:0, y: 0)
//This does not find the satellite nodes
let satellites = children.flatMap { $0 as? Satellite }
//This does work
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "satellite") {
self.satTuple.0 = node.position
self.satTuple.1 = (node as? SKSpriteNode)!
self.currentSatellite.append(self.satTuple)
}
}
The Obstacle class
First of all you should create an Obstacle class like this.
class Obstacle: SKSpriteNode { }
Now into the scene editor associate the Obstacle class to your obstacles images
The Player class
Do the same for Player, create a class
class Player: SKSpriteNode { }
and associate it to your player sprite.
Checking for collisions
Now into GameScene.swift change the updated method like this
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
let obstacleNearSprite = obstacles.contains { (obstacle) -> Bool in
let distance = hypotf(Float(player.position.x) - Float(obstacle.position.x), Float(player.position.y) - Float(obstacle.position.y))
return distance < 100
}
if obstacleNearSprite {
print("Oh boy!")
}
}
What does it do?
The first line retrieves all your obstacles into the scene.
the second line retrieves the player (and does crash if it's not present).
Next it put into the obstacleNearSprite constant the true value if there is at least one Obstacle at no more then 100 points from Player.
And finally use the obstacleNearSprite to print something.
Optimizations
The updated method gets called 60 times per second. We put these 2 lines into it
let obstacles = children.flatMap { $0 as? Obstacle }
let player = childNodeWithName("player") as! Player
in order to retrieves the sprites we need. With the modern hardware it is not a problem but you should save references to Obstacle and Player instead then searching for them in every frame.
Build a nice game ;)
you will have to loop through the children of the scene and assign them to local objects to use in your code
assuming your objects in your SKS file were named Obstacle1, Obstacle2, Obstacle3
Once in local objects you can check and do whatever you want with them
let obstacle1 = SKSpriteNode()
let obstacle2 = SKSpriteNode()
let obstacle3 = SKSpriteNode()
let obstacle3Location = CGPointZero
func setUpScene() {
self.enumerateChildNodesWithName("//*") {
node, stop in
if (node.name == "Obstacle1") {
self.obstacle1 = node
}
else if (node.name == "Obstacle2") {
self.obstacle2 = node
}
else if (node.name == "Obstacle3") {
self.obstacle3Location = node.position
}
}
}
So I have this code from the tutorial:
func didBeginContact(contact: SKPhysicsContact) {
// 1
var firstBody: SKPhysicsBody?
var secondBody: SKPhysicsBody?
var body: SKPhysicsBody
//contact.bodyB.node!.physicsBody!.allContactedBodies().count
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
println("1 = A, 2 = B")
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
println("2 = A, 1 = B")
}
// 2
if ((firstBody!.categoryBitMask & PhysicsCategory.Monster != 0) &&
(secondBody!.categoryBitMask & PhysicsCategory.Projectile != 0)) {
for var c = 1; c <= contact.bodyB.node!.physicsBody!.allContactedBodies().count; c++ {
projectileDidCollideWithMonster(firstBody!.node as! SKSpriteNode, monster: secondBody!.node as! SKSpriteNode)
}
secondBody!.node?.removeFromParent()
}
}
func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
println("Hit")
changeScore(1)
changeAmo(true)
projectile.removeFromParent()
monster.removeFromParent()
}
Then what is happening is that a projectile sometimes hit TWO monsters at once.
When this happens - didBeginContact method crashes saying that secondBody is nil.
After a thorough research I found out the reason:
when projectile collides with two other nodes at once - this method runs two times. After the first run - if gets bodyA as projectile, bodyB as a monster - passes them on to projectileDidCollideWithMonster and it removes them both. then it runs immediately again but at that moment projectile doesn't exist anymore and it crashes not able to assign it's node.
I have no idea how to overcome this:( any suggestions, please?
SOLUTION: Thanks to ideas below i did some changes.
added and array and a trigger at the top of the class:
var bodiesToBeRemoved = [SKSpriteNode]()
var shouldRemoveBodies = false
and did these modifications:
func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
/* Called when collisions is detected and collided nodes are passed to it */
//score and stuff
println("Hit")
changeScore(1)
changeAmo(true)
//collect the nodes to be removed
bodiesToBeRemoved.append(projectile)
bodiesToBeRemoved.append(monster)
//change the trigger to remove collected nodes later on
shouldRemoveBodies=true
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
//check the trigger
if shouldRemoveBodies == true {
println("\(bodiesToBeRemoved.count)")
//remove collected nodes
for var i = 0; i<bodiesToBeRemoved.count; i++ {
bodiesToBeRemoved[i].removeFromParent()
}
//reset array
bodiesToBeRemoved.removeAll(keepCapacity: false)
//reset the trigger
shouldRemoveBodies = false
} else {
//do nothing:)
}
}
Instead of removing your projectile immediately, just mark it for removal (e.g. by setting a boolean flag, or adding it to some collection if it's not already in the collection). Then later, before the next physics check (e.g. at the end of this frame), go through and remove all projectiles marked for removal.
Just check it's nil or not better than check every nodes's mark.
func removeNodeFromPhysicsBody(ps: SKPhysicsBody){
if (ps.node != nil){
ps.node?.removeFromParent()
}
}
func wallDidCollideWithMeteor(wall:SKPhysicsBody, meteor:SKPhysicsBody) {
removeNodeFromPhysicsBody(wall)
removeNodeFromPhysicsBody(meteor)
}