Upcoming and OnDemand Webinars View full list

tvOS Games, Part 4: Bullets and Explosions

Steve Sparks

In the previous installments of this series, I explore game controllers, create a SpriteKit-driven game, and animate the sprites. Now we get to the important part: shooting. In this installment, I’ll walk through creating the bullet and detecting when it hits something.

In the original game, moving the left joystick moved the player around the screen, and moving the right joystick would shoot bullets. A talented Robotron player would usually have his (or her) right joystick at a 90º angle to his left joystick, orbiting an ever tightening-group of bad guys:

The player gets to shoot every fifth step with no pauses and no reloading. On the enemy side, there’re some interesting play dynamics:

  • Foot Soldiers (red andyellow) shoot fairly infrequently, perhaps 5% or less.
  • Hulks (big green chest) are indestructible, wander like civilians, and stomp civilians. (They get a nice skull-n-crossbones avatar for a moment, too!) The player’s bullets will cause the hulk to skip a walking turn.
  • Spheroids move to the corners and launch little Enforcers.
  • Enforcers shoot slightly more intelligently, sometimes leading the player for the hit.
  • Brains (not shown) wander and touch civilians, who become Progs (also not shown.)

I made FootSoldier a subclass of Enemy, and as of now, that’s the only type of bad guy implemented. Perfectly happy to consider all pull requests!

When the characters fire, they create a laser bullet centered on them, and send it hurtling in the direction chosen. The bullet is a 5×30 sprite, rotated to the appropriate angle.

class Bullet : GameNode {
    static var bullets : [Bullet] = []
    static func aimedAt(_ vector: CGVector, by shooter: GameNode) -> Bullet {
        let size = CGSize(width: 5, height: 30)

        // Put a bullet on the screen
        let bullet = Bullet(texture: nil, color: UIColor.red, size: size)
        bullet.position = shooter.position
        bullet.rotateTo(vector)
        bullets.append(bullet)
        shooter.universe.addChild(bullet)

        // and shoot it!
        let bulletAction = SKAction.sequence([
            SKAction.playSoundFileNamed("laser.wav", waitForCompletion: false),
            SKAction.move(by: vector.bulletVector, duration: 2.5),
            SKAction.removeFromParent()
            ])
        bullet.run(bulletAction, completion: {
            if let idx = bullets.index(of: bullet) {
                bullets.remove(at: idx)
            }
        })
        return bullet
    }

    /* 180º = π radians; 90º = π/2 radians; 45º = π/4 radians */
    /* also for these bullets 270º looks identical to 90º     */
    
    func rotateTo(_ vector: CGVector) {
        var angle = 0.0
        let sv = vector.simplifiedVector

        if sv.dy == sv.dx && sv.dx != 0 {
            angle -= .pi / 4
        } else if sv.dy == -1*sv.dx && sv.dx != 0 {
            angle += .pi / 4
        }
        if sv.dx != 0 && sv.dy == 0 {
            angle += .pi / 2
        }
        zRotation = CGFloat(angle)
    }
}

Once I’ve placed the object on the screen, I move it for 2.5 seconds in the direction of the bulletVector, which is (2000 * x, 2000 * y). When it gets to the end, I remove it. Here’s the Enemy’s shoot() function:

func shoot() -> Bullet? {
    guard !dead else {
        return nil
    }
    if( (arc4random() % 20) == 0 ) { // 5% chance
        let shotVector = universe.directionToNearestPlayer(from: self)
        let shot = Bullet.aimedAt(shotVector, by: self)
        return shot
    }
    return nil
}

Easy peasy. Looks like a bullet, acts like a bullet, except for the killing part.

Physics

SpriteKit has a physics engine that will handle both contacts and collisions. What’s the distinction, you ask? Contacts notify us of intersection, while collisions actually affect each other’s trajectory. To demonstrate, here I set the collision mask for the bullet, and then set its mass property to 10,000. As they say, hijinks ensue!

bounce

Needless to say, this wasn’t the effect I was looking for. I don’t want my bullets to bounce off my targets… I want them to blow up. So the code will be doing a contact test.

In any event, both methods work off of a bitmask system based on a UInt32. Rather than use a nice Swift-y OptionSet, I stuck with the old fashioned method. I described my few different object types:

enum CollisionType : UInt32 {
    case Player = 0x01
    case Enemy = 0x02
    case Civilian = 0x04
    case Bullet = 0x08
    case Wall = 0x10
}

Going back to the bullet code, just before the shot fires, I add an SKPhysicsBody to the new bullet.

// give it properties for letting us know when it hits
let body = SKPhysicsBody(rectangleOf: size)
body.categoryBitMask = CollisionType.Bullet.rawValue
body.collisionBitMask = 0x0
if let _ = shooter as? Player {
    body.contactTestBitMask = CollisionType.Enemy.rawValue | CollisionType.Civilian.rawValue
} else {
    body.contactTestBitMask = CollisionType.Player.rawValue | CollisionType.Civilian.rawValue
}
bullet.physicsBody = body

Of course I then modified our Player, Civilian and Enemy classes to set their physics body appropriately as well. The next step is to implement a delegate method to get notified of the contact.

extension GameUniverse : SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        var hit : Hittable?
        var bullet : Bullet?
        
        if let shot = contact.bodyA.node as? Bullet {
            hit = contact.bodyB.node as? Hittable
            bullet = shot
        } else if let shot = contact.bodyB.node as? Bullet {
            bullet = shot
            hit = contact.bodyA.node as? Hittable
        } else if let p1 = contact.bodyA.node as? Player,
            let p2 = contact.bodyB.node as? Enemy {
            gameEndedByTouchingPlayer(p1, enemy: p2)
        } else if let p1 = contact.bodyB.node as? Player,
            let p2 = contact.bodyA.node as? Enemy {
            gameEndedByTouchingPlayer(p1, enemy: p2)
        }

If the collision was the player bumping into an enemy, there’s a method for that. Otherwise the hit variable will contain the hittable that got shot, and the bullet variable contains the bullet that got ‘im.

If it’s an enemy who got shot, I blow him up and remove him from the array of enemies. (I considered calling it the “enemies list” but that was too… political.) If that was the last enemy, you cleared the level. If it was the player, end the level. And if it was a civilian, blow ‘em up. If it’s the last friendly, do something. (I stubbed out gameEndedByNoMoreFriendlies() because I think maybe we should give a “clear the room” award in such a case!)

        if let target = hit, let bullet = bullet {
            bullet.removeFromParent()
            if let enemy = target as? Enemy {
                score += enemy.pointValue
                blowUp(enemy)
                enemy.dead = true
                if let enemyIdx = enemies.index(of: enemy) {
                    enemies.remove(at: enemyIdx)
                }
                if allDead() {
                    showLabel("LEVEL COMPLETE") {
                        self.stateMachine?.win()
                    }
                }
                enemy.removeFromParent()
            } else if let civ = target as? Civilian {
                blowUp(civ)
                civ.removeFromParent()
                if let friendlyIndex = friendlies.index(of: civ) {
                    friendlies.remove(at: friendlyIndex)
                }
                if friendlies.count == 0 {
                    gameEndedByNoMoreFriendlies()
                }
            } else if let player = target as? Player {
                guard stateMachine?.currentState != stateMachine?.lost else {
                    return
                }
                gameEndedByShootingPlayer(player, bullet: bullet)
            } else {
                print("Something funky")
            }
        }
    }
    
    func allDead() -> Bool {
        if enemies.count == 0 {
            return true
        }
        for enemy in enemies {
            if (!enemy.dead) {
                return false
            }
        }
        return true
    }
}

And now our enemies are dying, though somewhat anticlimactically. We’ll get to that in a second…

Walls and Bouncing

Before I move on, I’ll remove that godawful wall logic from the Movable.move() method and make that a genuine contact test as well. One of the yak-shave areas I didn’t cover is the game generation section, but I added the appropriate methods there. I want big red walls! Back in my GameUniverse class, I added an internal Wall class and then methods to populate them.

class Wall : SKSpriteNode {}

func addBorder() {
    let mySize = self.frame.size
    addWall(CGRect(x:0, y:0, width: screenBorderWidth, height: mySize.height))
    addWall(CGRect(x:mySize.width-screenBorderWidth, y:0, width: screenBorderWidth, height: mySize.height))

    addWall(CGRect(x:0, y:0, width: mySize.width, height: screenBorderWidth))
    addWall(CGRect(x:0, y:mySize.height-screenBorderWidth, width: mySize.width, height: screenBorderWidth))
}

func addWall(_ rect: CGRect) {
    let wallNode = Wall(color: UIColor.red, size: rect.size)
    let bod = SKPhysicsBody(rectangleOf: rect.size)
    bod.affectedByGravity = false
    bod.pinned = true
    bod.friction = 100000
    bod.linearDamping = 1000
    bod.angularDamping = 1000
    bod.contactTestBitMask = CollisionType.Player.rawValue | CollisionType.Civilian.rawValue
    bod.categoryBitMask = CollisionType.Wall.rawValue
    bod.collisionBitMask = 0x00
    wallNode.physicsBody = bod
    let center = CGPoint(x: rect.midX, y: rect.midY)
    wallNode.position = center
    addChild(wallNode)
}

Back in our didBegin(contact:) method:

if let wall = contact.bodyA.node as? Wall,
    let walker = contact.bodyB.node as? Movable {
    walker.revert(wall)
} else if let wall = contact.bodyB.node as? Wall,
    let walker = contact.bodyA.node as? Movable  {
    walker.revert(wall)
} ...

The revert() method in Movable just takes the previousPosition and assigns it to position. We’ll want that, but we also need to change our walker’s direction to walk away from the wall.

Originally, I just took the reverse() of the current direction and applied that. And, often, that was perfect. But occasionally the walkers would continue heading right off the screen, and sometimes they’d appear to walk while embedded in the walls. It wasn’t until much later that I realized I’d gotten myself into a race condition: I updated position, and THEN decremented the step counter which might effect a direction change. On cases where the collision with a wall coincided with a new random direction, it was going to go wonky.

I next tried to figure out which quadrant of the screen you were in, and choose the correct direction for that. But that math got unwieldy quickly when the screen’s aspect ratio came in. In the end, the old-fashioned way worked best: Ask the collided-with wall which direction the player should choose.

class Wall : SKSpriteNode {
    enum WallType {
        case north, south, east, west
    }
    var type : WallType = .north

    var safeDirection : Movable.WalkDirection {
        switch(type) {
        case .north : return .south
        case .south : return .north
        case .east : return .west
        case .west : return .east
        }
    }
}

And back in the Civilian class:

override func revert(_ obstacle: SKSpriteNode) {
    super.revert(obstacle)
    if let wall = obstacle as? Wall {
        direction = wall.safeDirection
    } else {
        direction = direction.reverse()
    }
    stepCount = 50
    walk()
}

OKAY! Let’s take stock. There’s animated sprites running around shooting each other. Our civilians are correctly bumping into walls and heading back in. It’s starting to feel like a real game!

Now I want to add an explosion effect to each hit. An alien shooter game is no fun without good explosions, of course.

SKEmitterNode Explosions

Xcode has a pretty nice editor for particle emitters in SpriteKit. To get into it, create a new SpriteKit particle file:
New file in SpriteKit

Xcode then asks for a template, and I chose Spark. From there, I found myself in an editor that shows the emitter running. After fiddling with the settings I came up with something that made me happy. I set the birthrate high, and the number of particles low, to give it a halo effect.

particle file

Using this emitter in your code is pretty simple. Create an emitter node and put it where you need it. Usually, it will copy the position of some parent node. For this explosion, I will want to remove it just before it restarts, to be sure it doesn’t show the explosion twice.

func explode(at point: CGPoint, for duration: TimeInterval, color: UIColor = UIColor.boomColor, completion block: @escaping () -> Swift.Void) {
    if let explosion = SKEmitterNode(fileNamed: "Explosion") {
        explosion.particlePosition = point
        explosion.particleColorSequence = SKKeyframeSequence(keyframeValues: [color], times: [1])
        explosion.particleLifetime = CGFloat(duration)
        self.addChild(explosion)
        // Don't forget to remove the emitter node after the explosion
        run(SKAction.wait(forDuration: duration), completion: {
            explosion.removeFromParent()
            block()
        })
    }
}

Now, I want each of the explosions to be slightly different in color and duration. Bad guys explode quickly; civilians explode more slowly (and in red because I’m morbid like that.) The player explodes much more slowly. Here’s that logic:

func blowUp(_ target: Hittable) {
    if let player = target as? Player {
        stopGame()
        player.alpha = 0
        explode(at: player.position, for: 2.5, color: UIColor.white) {
            player.alpha = 1
        }
        run(SKAction.playSoundFileNamed("player-boom.wav", waitForCompletion: false))
    } else if let enemy = target as? Enemy {
        run(SKAction.playSoundFileNamed("enemy-boom.wav", waitForCompletion: false))
        explode(at: enemy.position, for: 0.25) {
        }
    } else if let civ = target as? Civilian {
        run(SKAction.playSoundFileNamed("civ-boom.wav", waitForCompletion: false))
        explode(at: civ.position, for: 0.5, color: UIColor.red) {
        }
    }
}

AHA! I snuck sounds in there! As you can see, there’s almost nothing to it. Drag your sound file into your project, and make sure it’s checked for your app target. Then, if your object is any sort of SKNode or SKScene subclass, it’s just one line of code:

run(SKAction.playSoundFileNamed("player-boom.wav", waitForCompletion: false))

SpriteKit caches the file so that subsequent playback events have no delay.

Next Up

At this point, we have a real game! Our characters are running around. We can kill good guys and bad guys and we can lose our life. The next step is to develop the disconnected levels into a single game with increasing difficulty. Stay tuned!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project