Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
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:
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.
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!
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…
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.
Xcode has a pretty nice editor for particle emitters in SpriteKit. To get into it, create a new SpriteKit particle file:
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.
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.
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!
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...