Getting Started With iOS for a C# Programmer - Part Five - Collision

January 07, 2018

In the previous post in this series, we covered moving an object around the screen. The next thing to consider to how to shoot the aliens, and how they can defeat the player.

Aliens

The first stage is to create something to collide with. As with other game objects, our aliens will simply be rectangles at this stage. Let’s start with a familiar looking function:

    func createAlien(point: CGPoint) -> SKShapeNode {
        let size = CGSize(width: 40, height: 30)
        
        let alien = SKShapeNode(rectOf: size)
    
        alien.position = point
        alien.strokeColor = SKColor(red: 0.0/255.0, green: 200.0/255.0, blue: 0.0/255.0, alpha: 1.0)
        alien.lineWidth = 4
        
        alien.physicsBody = SKPhysicsBody(rectangleOf: size)
        
        alien.physicsBody?.affectedByGravity = false
        alien.physicsBody?.isDynamic = true
        
        alien.name = "alien"
        
        return alien

    }

So, that will create us a green rectangle - let’s cause them to appear at regular intervals:

    override func didMove(to view: SKView) {
        createScene()
        
        createAlienSpawnTimer()
    }

    func createAlienSpawnTimer() {
        var timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.timerUpdate), userInfo: nil, repeats: true)
        
    }

The scheduleTimer calls self.timerUpdate:


    @objc func timerUpdate() {
        let xSpawn = CGFloat(CGFloat(arc4random\_uniform(1334)) - CGFloat(667.0))
        let ySpawn = CGFloat(250)
        
        print (xSpawn, ySpawn)
        
        let spawnLocation = CGPoint(x: xSpawn, y: ySpawn)
        let newAlien = createAlien(point: spawnLocation)
        self.addChild(newAlien)
    }

So, every second, we’ll get a new alien… But they will just sit there at the minute; let’s get them to try and attack our player:


    override func update(\_ currentTime: TimeInterval) {
        // Called before each frame is rendered
        player?.position.x += playerSpeed!
        
        self.enumerateChildNodes(withName: "bullet") {
            (node, \_) in
            node.position.y += 1
        }
        
        moveAliens()
    }
    
    func moveAliens() {
        self.enumerateChildNodes(withName: "alien") {
            (node, \_) in
            node.position.y -= 1
            if (node.position.x < (self.player?.position.x)!) {
                node.position.x += CGFloat(arc4random\_uniform(5)) - 1 // Veer right
            } else if (node.position.x > (self.player?.position.x)!) {
                node.position.x += CGFloat(arc4random\_uniform(5)) - 4 // Veer left
            }
        }
    }

Collision

The SpriteKit game engine actually handles most of the logic around collisions for you. There’s a few changes that are needed to our game at this stage, though.

SKPhysicsContactDelegate

This is the parent class that actually handles the collision logic, so your GameScene class now needs to look more like this:

class GameScene: SKScene, SKPhysicsContactDelegate {

The game engine needs to be told where this SKPhysicsContactDelegate implementation is; in our case, it’s the same class:

    func createScene(){
        self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
        self.physicsBody?.isDynamic = false
        self.physicsBody?.affectedByGravity = false
        
        self.physicsWorld.contactDelegate = self
        self.backgroundColor = SKColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0)

Contact and Colision Masks

The next thing, is that you need to tell SpriteKit how these objects interact with each other. There are three concepts here: contact, collision and category.

Category

This allows each object to adhere to a type of behaviour; for example, the aliens need to pass through each other, but not through bullets; likewise, if we introduced a different type of alien (maybe a different graphic), it might need the same collision behaviour as the existing ones.

Contact

The idea behind contact is that you get notified when two objects intersect each other; in our case, we’ll need to know when aliens intersect bullets, and when aliens intersect the player.

Collision

Collision deals with what happens when the objects intersect. Unlike contact, this isn’t about getting notified, but the physical interaction. Maybe we have a game where blocks are pushed out of the way - in which case, we might only need collision, but not contact; or, in our case, we don’t have any physical interaction, because contact between two opposing entities results in one of them being removed.

Code

So, the result of all that is that we need three new properties setting for each new object:


        alien.physicsBody?.categoryBitMask = collisionAlien
        alien.physicsBody?.collisionBitMask = 0
        alien.physicsBody?.contactTestBitMask = collisionPlayer
        
        alien.name = "alien"

        bullet.physicsBody?.categoryBitMask = collisionBullet
        bullet.physicsBody?.collisionBitMask = 0
        bullet.physicsBody?.contactTestBitMask = collisionAlien
        
        bullet.name = "bullet"

        player.physicsBody?.categoryBitMask = collisionPlayer
        player.physicsBody?.collisionBitMask = 0
        player.physicsBody?.contactTestBitMask = 0
        
        player.name = "player"

didBegin

One this is done, you have access to the didBegin function; which, bizarrely named as it is, is the function that handles contact. Before we actually write any code in here, let’s create a helper method to determine if two nodes have come into contact:

    func AreTwoObjectsTouching(objA: String, nodeA: SKNode, objB: String, nodeB: SKNode, toRemove: String) -> Bool {
        if (objA == nodeA.name && objB == nodeB.name) {
            if (toRemove == objA) {
                RemoveGameItem(item: nodeA)
            } else if (toRemove == objB) {
                RemoveGameItem(item: nodeB)
            }
            return true
        } else if (objB == nodeA.name && objA == nodeB.name) {
            if (toRemove == objA) {
                RemoveGameItem(item: nodeB)
            } else if (toRemove == objB) {
                RemoveGameItem(item: nodeA)
            }
            return true
        } else {
            return false
        }
    }

Since the accepted standard for iOS is bad naming, I felt it my duty to continue the tradition. This helper method is effectively all the logic that occurs. As you can see, as we don’t know what has touched what, we have reversed checks and then simply remove the stated item (in our case, that is just a game rule). The didBegin function simply calls this:


    func didBegin(\_ contact: SKPhysicsContact) {
        print ("bodyA", contact.bodyA.node?.name)
        print ("bodyB", contact.bodyB.node?.name)
        
        // If player and alien collide then the player dies
        if (AreTwoObjectsTouching(objA: "alien", nodeA: contact.bodyA.node!,
                                  objB: "player", nodeB: contact.bodyB.node!, toRemove: "player")) {
        
        } else if (AreTwoObjectsTouching(objA: "bullet", nodeA: contact.bodyA.node!,
                                         objB: "alien", nodeB: contact.bodyB.node!, toRemove: "alien")) {
            RemoveGameItem(item: contact.bodyB.node!)
            RemoveGameItem(item: contact.bodyA.node!)
        }
     
    }

The debug statements at the top are clearly not necessary; however, they do give some insight into what is happening in the game.

Summary

We now have a vaguely working game:

x code collision

You can control the ship, fire, and the aliens are removed when they are hit, along with the player being removed when it is hit. The next stage is to replace the rectangles with images.

References

https://stackoverflow.com/questions/46494701/understanding-spritekit-collisionbitmask



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024