5

OK - I’m sure this is a duplicate and that Whirlwind or KnightOfDragons has posted a solution, but I can’t find it.

I’m writing a clone of Space Invader in Swift using Sprite-Kit. The problem I have is that when a invader bomb hits my ship, the code sometimes registers 2 or 3 collisions, immediately ending the game. Here’s the section of ‘didBeginContact’ that handles the bomb/ship collision:

    case category.bomb.rawValue | category.ship.rawValue:
        print("Bomb hit ship!")
        let bomb = contact.bodyA.categoryBitMask == category.bomb.rawValue ? contact.bodyA.node : contact.bodyB.node
        bomb?.physicsBody?.contactTestBitMask = 0 // Seems to prevent multiple contacts with same bomb
        ship.physicsBody!.contactTestBitMask = 0 // We'll reset this after the death animation
        bomb?.removeAllActions()
        ship.removeAllActions()
        bomb?.removeFromParent()
        ships -= 1

        if ships == 0 { endScene(won: false, withMessage: "A bomb got you!") }

and when I run the game I see:

Bomb hit ship!
2 ships left.

after 1 bomb hit (this is correct)

Bomb hit ship!
1 ships left.
Bomb hit ship!
0 ships left.

after a 2nd bomb hit (which is wrong).

I never have the contact NOT being registered, and sometimes (50%?) it works perfectly. Other times I’ve seen myself with -4 ships! I’m sure it’s something obvious/fundamental that I’m getting wrong though.

My comments about setting the contactTestBitMask to 0 for the bomb and the ship are obviously wrong. I know this shouldn’t be necessary, as I remove the bomb when the contact occurs, so it shouldn’t occur again.

How can I guarantee that the contact is only processed once?

================================

Update: I added 2 print statements to provide more debugging information:

        print("bodyA is \(contact.bodyA.node!.name)")
        print("bodyB is \(contact.bodyB.node!.name)")

This is after the let bomb = contact.bodyA.category... statement and now I get:

Bomb hit ship!
bodyA is Optional("bomb")
bodyB is Optional("playerShip")
1 ships left.

after 1 bomb hit, and :

Bomb hit ship!
fatal error: unexpectedly found nil while unwrapping an Optional value

after a 2nd bomb hit. So after the second collision, bodyA is nil, so I don't see why Sprite-Kit has actually registered a collision?

Any ideas?

Steve Ives
  • 7,894
  • 3
  • 24
  • 55
  • Is the answer that Sprite-Kit has queued multiple collision to be processed between the ship and the bomb (which it shouldn't but hey, who knows) and then SK calls dBC for each collision and because I removed the bomb the first time, it's nil the second and subsequent times. So I need to flag the bomb as active when it is dropped, check if the bomb is active when I detect a collision and set it to inactive rather than removing it in dBC? – Steve Ives Sep 15 '16 at 08:36
  • From what I've seen, didBeginContact is fired multiple times when there are multiple contact points between two bodies..This may happen when you create body from texture or even if bodies are circular. I can't really find my own answers at the moment (i am on phone) but this one explains crash you are experiencing: http://stackoverflow.com/a/37003007 There are few ways to slove your problem. One would be to check if node.parent is nil and return from didBeginContact if true. The other way is a flag on a bullet node. Probably some other ways, i don't remember more. – Whirlwind Sep 15 '16 at 13:35
  • yes what @Whirlwind said, remember, you really do not want to be removing from parent during the didcontactbegin phase, because there may be instances when you need the node to still be in the scene for a second contact (One bullet hits 2 overlapping enemies, maybe you want a ghost, who knows) I personally like reserving categoryBitMask bit 31 to handle alive or dead, and during physics checking, skip over these nodes – Knight0fDragon Sep 15 '16 at 13:39
  • @Knight0fDragon Perhaps, but I don't want the bomb to hang around - I want it gone. There's definitely only 2 nodes involved and I can't imagine what's happening under the covers. In a test project, I've even tried a 3 way collision by placing 3 sprite nodes on top of each other and dBC still behaves itself, being called 3 times: one for the collision between A & B, once for the once between B & C and again for the one between A & c. If I remove node A on the first collision, it still all works... – Steve Ives Sep 15 '16 at 14:15
  • @Whirlwind - what I'm seeing matches what you describe - multiple contacts between 2 bodies (both created from textures). Not sure how dBC being called with with one of the bodies being nil fits into the scheme of things. – Steve Ives Sep 15 '16 at 14:15
  • @Whirlwind already explained the multiple contacts issue, and even though you want the bomb gone, the code is not designed to do that. You need to handle it properly. This is like trying to fix a car engine while it is still running. You need to wait till the engine is off before making repairs. You can note what is happening, then modify the engine after so that it can correct itself, but don't go sticking your hands in that engine while it is running. – Knight0fDragon Sep 15 '16 at 14:23
  • That is why it is important to flag what is being removed, and remove it during the update end phase. Which is why I use categoryBitMask bit 31. I mark it, then in the contactphase, I check if it exists, and if it does, do not do any processing. Then by sticking with this manner, if I do a new game, and want a ghost, I can do it with ease, because my style of code allows it – Knight0fDragon Sep 15 '16 at 14:26
  • You could also turn off the bit in the categoryBitMask if you have no intention of ever using that sprite again, allowing the physics world to check 1 less node – Knight0fDragon Sep 15 '16 at 14:27
  • I need to look into this more - Apple call removeFromParent within didBeginContact in their own sample code. – Steve Ives Sep 15 '16 at 14:44
  • Apple does a lot of things they shouldn't be doing, Like having scenes present other scenes using scene.view. it is a shame that SKView is not called SceneController – Knight0fDragon Sep 15 '16 at 16:05

1 Answers1

1

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.

Steve Ives
  • 7,894
  • 3
  • 24
  • 55
  • this doesn't help you, you just got lucky. What if bodyA was the bomb? This means bodyA is now nil, which means you will crash on `let bomb = contact.bodaA.categoryBitMask` – Knight0fDragon Sep 15 '16 at 13:44
  • I thought bomb would be set to contact.bodyA.node if contact.bodyA.categoryBitMask == category.bomb.rawValue, else it would be set to contact.bodyB.node – Steve Ives Sep 15 '16 at 14:17
  • E.G. bodyA is bomb. In the first pass you killed bomb, In the secondPass bodyA is now dead so it is nil. You will now crash at `contact.bodyA.categoryBitMask == category.bomb.rawValue` because you are doing `nil.categoryBitMask == category.bomb.rawValue` which is not allowed because contact.bodyA is not an optional. If it is an optional, then bomb becomes the other sprite, which also is undesired – Knight0fDragon Sep 15 '16 at 14:21
  • @Knight0fDragon going back a bit, but one of the contact.bodyX.nodes can definitely be set to 'nil' during a contact, although the 'contactTestBitMask' is still set. One the first contact both nodes are present but dBC is called 3 further times with one body being 'nil', which I test for and return to prevent multiple hits being registered. – Steve Ives Nov 24 '16 at 08:42
  • It being set to nil is not the problem, you need to handle the fact that it is nil – Knight0fDragon Nov 24 '16 at 18:40
  • @Knight0fDragon I just {return} if it's 'nil' so I don't process the collision again. For other collisions registering multiple times, but where I don't remove the sprite involved, I check its 'active' property. Seems to be working and the code looks clean. – Steve Ives Nov 24 '16 at 19:45
  • ok steve, but what do you do when 2 collisions hit and you do not want node to be nil till both have processed – Knight0fDragon Nov 25 '16 at 12:21
  • @Knight0fDragon What's the scenario? One object legitimately hitting 2 others and you want to process both of them before removing the first object? Or SK generating 2 calls to dBC in one update loop for one collision between 2 objects? – Steve Ives Nov 25 '16 at 12:26
  • they are both the same scenario – Knight0fDragon Nov 25 '16 at 12:27
  • @Knight0fDragon Are you sure? When my bomb hit my ship that's one collision to me, but SK generated multiple dBC calls, all in the same game loop. But if I decide that my missile can destroy up to 2 invaders before it itself is destroyed, then I need to track that the missile has hit one thing and then remove or reset it on the second it. – Steve Ives Nov 25 '16 at 12:30
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/129035/discussion-between-steve-ives-and-knight0fdragon). – Steve Ives Nov 25 '16 at 12:30