I'm building a sprite kit game in swift and I need the score to increase by 1 when collision between 2 nodes is detected. The score is stored in a variable named animalsCount and is outputted to a label node:
//Score count in stats bar
//Animal score count
animalsCount = 0
animalsCountLabel.text = "\(animalsCount)"
animalsCountLabel.fontSize = 45
animalsCountLabel.fontColor = SKColor.blackColor()
animalsCountLabel.position = CGPoint (x: 630, y: 40)
addChild(animalsCountLabel)
The two sprite nodes that are colliding are savior and chicken1. Right now, I am keeping score and detecting collision using the following code:
func didBeginContact(contact: SKPhysicsContact) {
//Chicken1
if (contact.bodyA.categoryBitMask == ColliderType.Savior.rawValue && contact.bodyB.categoryBitMask == ColliderType.Chicken1.rawValue ) {
println("chicken1 contact made")
chicken1.hidden = true
chicken1.setScale(0)
animalsCount++
animalsCountLabel.text = "\(animalsCount)"
} else if (contact.bodyA.categoryBitMask == ColliderType.Chicken1.rawValue && contact.bodyB.categoryBitMask == ColliderType.Savior.rawValue) {
println("chicken1 contact made")
chicken1.hidden = true
chicken1.setScale(0)
}
Score is not increased in the else if statement because it can't happen in my game.
The problem is that animalsCount increases by 2, not 1, every time savior and chicken1 collide.
After some troubleshooting, I found out this is NOT because the score is being increased for both of the colliding bodies. This is not the case because only 1 line of code is ever satisfied. This is the only line that is satisfied:
if (contact.bodyA.categoryBitMask == ColliderType.Savior.rawValue)
The score goes up by 2 instead of 1 because savior seems to "bounce" off of chicken1 so that contact.bodyA.categoryBitMask is set equal to ColliderType.Savior.rawValue TWICE every time collision appears to occur ONCE.
I don't know how to fix this problem. How do I make it so that collision is only detected ONCE and so the score is only increased once?
I eventually solved the problem using an Int variable that controlled if statements so collision could only be detected once until the sprite node cycled through and the variable was reset.
I declared a variable called chickenHasBeenGrabbed and set it at 0 initially. Once collision had been detected that first time, I set chickenHasBeenGrabbed to 1. Only after chickenHasBeenGrabbed was set back to 0 could collision be detected again:
func didBeginContact(contact: SKPhysicsContact) {
//Chicken1
if chickenHasBeenGrabbed == 0 {
if (contact.bodyA.categoryBitMask == ColliderType.Savior.rawValue && contact.bodyB.categoryBitMask == ColliderType.Chicken1.rawValue ) {
println("chicken1 contact made")
chicken1.hidden = true
chicken1.setScale(0)
animalsCount += 1
animalsCountLabel.text = "\(animalsCount)"
chickenHasBeenGrabbed = 1
} else if (contact.bodyA.categoryBitMask == ColliderType.Chicken1.rawValue && contact.bodyB.categoryBitMask == ColliderType.Savior.rawValue) {
println("chicken1 contact made")
chicken1.hidden = true
chicken1.setScale(0)
}
}
else if chickenHasBeenGrabbed == 1 {
if (contact.bodyA.categoryBitMask == ColliderType.Savior.rawValue && contact.bodyB.categoryBitMask == ColliderType.Chicken1.rawValue ) {
println("nothing to do; chicken was already grabbed!")
} else if (contact.bodyA.categoryBitMask == ColliderType.Chicken1.rawValue && contact.bodyB.categoryBitMask == ColliderType.Savior.rawValue) {
println("nothing to do; chicken was already grabbed!")
}}
Related
In order to prevent a lot of the same code, I want to refer to every single node(all children of the same parent) on the scene with one call. I have a gate at the bottom of the scene and I want to detect if any node(many different nodes getting added to the scene) passes the y position of this gate.
This is how my update function looks now:
override func update(_ currentTime: TimeInterval) {
// code I want to simplify
if Cap.position.y < illustration2.position.y
|| Cap2.position.y < illustration2.position.y
|| Cap3.position.y < illustration2.position.y
|| Cap4.position.y < illustration2.position.y
|| Cap5.position.y < illustration2.position.y
|| Cap6.position.y < illustration2.position.y
|| Cap7.position.y < illustration2.position.y
|| Cap8.position.y < illustration2.position.y
|| Cap9.position.y < illustration2.position.y
|| Cap10.position.y < illustration2.position.y {
// Transitioning to game over
let transition = SKTransition.crossFade(withDuration: 0)
let gameScene = GameOver(size: self.size)
self.view?.presentScene(gameScene, transition: transition)
}
}
All Caps are children of illustration2, so I was thinking about something like:
//if the y position of any child is below the illustration2 y position the game over scene starts
if dontaiIllustration2.children.position.y < dontaiIllustration2.position.y {
}
Or another approach could be to check that the illustration2 node has the lowest y position from all nodes at the scene.
You can’t. You need to place them into a collection (array, dictionary, set, etc) and then find a way to traverse through them with something like foreach, map, compactMap (formally flatMap), reduced, etc. in your case, I would use reduce.
let caps = [Cap1,Cap2,Cap3,Cap4,Cap5,Cap6,Cap7,Cap8,Cap9,Cap10]
let result = caps.reduce(false,{value,cap in return value || cap.position.y < illustration2.position.y})
if result {
// Transitioning to game over
let transition = SKTransition.crossFade(withDuration: 0)
let gameScene = GameOver(size: self.size)
self.view?.presentScene(gameScene, transition: transition)
}
What this is doing, is going through each of your nodes, and evaluating the current equation with the previous result.
So we start at a false, then it will check if cap y > illustration y. If true, then our value becomes true, and this value carries over to the next cap. It repeats this until you run out of caps, and then returns the final value, which will be a false or a true.
Not really proud of this but you can do a forEach on the children of a parent node.
let node = SKNode()
var checker = false
node.children.forEach { (node) in
if node.position.y < illustration2.position.y {
checker = true
}
}
if checker {
// Game over
}
or you can enumerate them if you only want targets with specific names:
node.enumerateChildNodes(withName: "all can have same name") { (node, stop) in
if node.position.y < illustration2.position.y {
checker = true
}
}
You could also one-line it by using filter:
node.children.filter({$0.position.y < illustration2.position.y}).count > 0
Either way, i would put this in an extension of SKNode.
I have two nodes, one "cat" and one "rat", but for some reason I can't get their collision to be detected. I'm using this method for masks:
enum CollisionTypes: UInt32 {
case holder = 1
case chef = 2
case powerups = 4
case ingredients = 8
case utensils = 16
case floor = 32
case bag = 64
case table = 128
case tip = 256
case rat = 512
case cat = 1024
}
Here is where I initialize their physics bodies:
// Cat physics body, the node's name is "cat"
public func initializeAt(position: CGPoint) {
sprite.position = position
sprite.zPosition = 5
sprite.name = "cat"
sprite.alpha = 0.7
scene.sceneContent.addChild(sprite)
sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
sprite.physicsBody!.isDynamic = false
sprite.physicsBody!.categoryBitMask = CollisionTypes.cat.rawValue
sprite.physicsBody!.contactTestBitMask = CollisionTypes.rat.rawValue
sprite.physicsBody!.collisionBitMask = CollisionTypes.rat.rawValue
// Rat physics body, the nodes name is "rat"
init() {
node.name = "rat"
node.zPosition = 5
node.physicsBody = SKPhysicsBody(rectangleOf: node.size)
node.physicsBody!.isDynamic = false
node.physicsBody!.categoryBitMask = CollisionTypes.rat.rawValue
node.physicsBody!.contactTestBitMask = CollisionTypes.cat.rawValue
node.physicsBody!.collisionBitMask = CollisionTypes.cat.rawValue
setupFrames()
}
Here is my didBegin() method. However, neither of the if statements get executed and I don't know why because I am using this method for a number of other things in my project.
func didBegin(_ contact: SKPhysicsContact) {
if let node1 = contact.bodyA.node as? SKSpriteNode,
let node2 = contact.bodyB.node as? SKSpriteNode {
if node1.name == "rat" && node2.name == "cat" {
for rat in rats {
if node1 == rat.node {
rat.die()
}
}
Cat.shared.resetPosition()
return
}
else if node1.name == "cat" && node2.name == "rat" {
for rat in rats {
if node2 == rat.node {
rat.die()
}
}
Cat.shared.resetPosition()
return
}
If I try playing around with the contactTestBitMasks and making them something different like "ingredients", then I can see that the cat and rat are interacting with ingredients but it seems like they just wont interact with eachother.
rats and cats won't trigger contacts with each other because both have isDynamic set to false. At least one of them needs to be dynamic before a contact is triggered.
From https://developer.apple.com/documentation/spritekit/skphysicsbody
The isDynamic property controls whether a volume-based body is
affected by gravity, friction, collisions with other objects, and forces or impulses you directly apply to the object.
I have a game with 10 levels and each level has an orb that a user can collect. When the user collects the orb the first time it should add +1 to the label and save using NSUserDefaults. That works fine but when I play the same level and I collect the orb again it shouldnt add another +1 to the label. I only need to add +1 for each orb in each of the levels. So that would be a total of 10 orbs saved in the label if the user collects all the orbs. What am I doing wrong?
class LevelOne: SKScene, SKPhysicsContactDelegate {
var didCollectOrb = true
override func didMove(to view: SKView) {
if didCollectOrb == true {
UserDefaults().set(UserDefaults().integer(forKey: "saveOrbs")+0, forKey:"saveOrbs")
print("will add nothing to label")
}
}
func didBegin(_ contact:SKPhysicsContact){
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
// 2. Assign the two physics bodies so that the one with the lower category is always stored in firstBody
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if firstBody.categoryBitMask == HeroCategory && secondBody.categoryBitMask == OrbCategory {
//saves orbs
if didCollectOrb {
UserDefaults().set(UserDefaults().integer(forKey: "saveOrbs")+1, forKey:"saveOrbs")
print("will add +1 to label")
}
}
}
}
Tracking the Int value in UserDefaults is probably not the best way since there is no innate duplication prevention. If a person completes level 1 10 times, then they have collected 10 orbs according to that UserDefaults value. Perhaps you would prefer a binary or boolean storing method. For example, an easy way to implement it would be either a dictionary or array, e.g.:
let levels = [false, false, false, false, false, false, false, false, false, false] //10 values for 10 levels
func completeLevel(level: Int) { //takes a level from 1-10
self.levels[level - 1] = true
}
Then completing a level multiple times:
completeLevel(level: 1)
completeLevel(level: 1)
completeLevel(level: 1)
completeLevel(level: 1)
completeLevel(level: 1)
Will still only show true at that level index. So getting the "total number of collected orbs" is something like:
func getOrbs() -> Int {
var orbs = 0
self.levels.forEach { orbs += $0 ? 1 : 0 }
return orbs
}
I wouldn't recommend just copying and pasting this since this isn't a great implementation. Just try to understand how this works so that you can find a better way of storing the score while preventing unwanted duplication of orb counts.
I’m following a tutorial for SpriteKit that has a problem with an IF statement. The logic of the line is as follows: If the bullet and the asteroid collide then remove them.
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid {
// remove bullet and asteroid
}
The problem arises when trying to make sure that the asteroid (body2.node) is inside the playable area before it can get shut down. For that, the author adds the following:
body2.node?.position.y < self.size.height
Making the complete IF statement as follows:
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid && body2.node?.position.y < self.size.height {
// remove bullet and asteroid
}
Apparently that line works with Swift 2 however Swift 3 makes a correction changing the position from an optional and force unwraps the position.
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid && body2.node!.position.y < self.size.height {
// remove bullet and asteroid
}
By force unwrapping the position, the app crashes “I THINK” when the three bodies collide. It is really difficult to tell when looking at the screen.
I’m testing the code below and I have not encounter any problems as of yet. Do you guys think that the fix below will work? What I'm thinking is, if I make sure the body2.node is not nil, then there is no reason why the app should crash since is not going to encounter a nil upon trying to force unwrap it.
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid {
// If the bullet has hit the asteroid
if body2.node != nil {
if ( body2.node!.position.y < self.size.height ) {
// remove bullet and asteroid
}
}
}
Or else, if there another way you guys can suggest a different way to write the original IF Statement?
Thanks
Yes, the if != nil statement (as it is currently written) will protect against force-unwrap-induced crashes.
An alternative is to use the if let syntax in Swift:
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid {
// If the bullet has hit the asteroid
if let body2Node = body2.node {
if body2Node.position.y < self.size.height {
// remove bullet and asteroid
}
}
}
The benefit is that it removes the ! from your code, and more clearly connects the nil check with the variable you are using later.
nathan has the right answer, but a better alternative would be to use a guard instead to protect your function:
...
guard let body1Node = body1.node, let body2Node = body2.node else {return}
//Beyond this point we need to guarentee both nodes exist
if body1.categoryBitMask == PhysicsCategories.bullet && body2.categoryBitMask == PhysicsCategories.asteroid {
// If the bullet has hit the asteroid
if body2Node.position.y < self.size.height {
// remove bullet and asteroid
}
}
By using a guard, we reduce nesting, and we know that beyond this point, the physics body must contain a node, unless we remove it before the function ends, which will eliminate any further need to check if the node exists.
As a side note, I always recommend to remove nodes at the end of your update phase to avoid issues like this, and instead just mark the node somehow that you will no longer be using it.
Hey so i have this game where you collect gems to unlock characters, i have the gems spawning and in my didBeginContct i have collision detecting setup, my problem is that it detects more than one touch and adds more than one gem! Please help i tried fixing it myself but i couldnt get the logic working right! Thanks in advance
func didBeginContact(contact: SKPhysicsContact) {
let fadeIn = SKAction.fadeAlphaTo(1, duration: 0.2)
let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
switch(contactMask) {
case colisionType.Player.rawValue | colisionType.Triangle.rawValue: {
}
case colisionType.Player.rawValue | colisionType.Diamond.rawValue:
diamond.removeFromParent()
gems++
default:
return
}
}