How I used Swift to build a simple touch game for iOS
Intro
How the heck do you make a simple touch game for iOS in Swift? I wasn’t quite sure, so I made this:
Let me show you how I built this, and share some gotcha’s that I’ve learnt along the way :)
Preface: I have only been doing Swift (in my leisure) for about a week now. I am sure there are better ways to do most of things shown here. I thought that sharing my journey as a beginner would be helpful for other people learning the language.
Initial Game Design
Initially, I spent some time trying to think of what kind of game I’d be capable of making that wouldn’t be that difficult to write a tutorial about, but also something that could act as a stepping stone towards a real game.
So the basic game design for Kamikaze type game I’m gonna call “Kamakazee” ended up as the following:
- Spawn planes/ship at the top of the screen
- Have them fly towards the bottom of the screen at an increasing pace.
- Touch the ships to have them destroyed/disappear.
- Show a score screen after 5 ships have crashed.
- Give the ability to retry and start over.
I have done some game dev before, but most of my work has been done in Unity with C#. Surprisingly, a lot of the concepts from Unity carry over to Swift quite nicely :)
Setting up our project
Lets create our project.
File -> New -> Project
Then select …
iOS -> Application -> Game
Make sure we are using Swift as our language, SpriteKit as our technology, and iPhone as our Device.
A Sprite is defined as a two-dimensional pre-rendered figure. Thus, SpriteKit will be our best choice for most 2D games. I hope to cover SceneKit, OpenGL ES, and Metal in future tutorials.
On the general tab, in our project settings, make sure we have portrait and upside down unselected for device orientation. We only want the user to be able to play this in landscape view.
Thats it! We are going to use Apple’s SpaceShip graphic that already comes with the default project, so no need to import any artwork. I encourage you to try adding graphics on your own.
PS -Friend’s faces work the best ;)
GameScene.swift - Complete Code
Lets open our GameScene.swift file.
Here is our complete game code. Yes, its a bit longer than my previous tutorials, but we are doing a lot of things. Don’t worry, I will fully explain EVERYTHING.
import SpriteKit
class GameScene: SKScene {
var score = 0
var health = 5
var gameOver : Bool?
let maxNumberOfShips = 10
var currentNumberOfShips : Int?
var timeBetweenShips : Double?
var moverSpeed = 5.0
let moveFactor = 1.05
var now : NSDate?
var nextTime : NSDate?
var gameOverLabel : SKLabelNode?
var healthLabel : SKLabelNode?
/*
Entry point into our scene
*/
override func didMoveToView(view: SKView) {
initializeValues()
}
/*
Sets the initial values for our variables.
*/
func initializeValues(){
self.removeAllChildren()
score = 0
gameOver = false
currentNumberOfShips = 0
timeBetweenShips = 1.0
moverSpeed = 5.0
health = 5
nextTime = NSDate()
now = NSDate()
healthLabel = SKLabelNode(fontNamed:"System")
healthLabel?.text = "Health: \(health)"
healthLabel?.fontSize = 30
healthLabel?.fontColor = SKColor.blackColor()
healthLabel?.position = CGPoint(x:CGRectGetMinX(self.frame) + 80, y:(CGRectGetMinY(self.frame) + 100));
self.addChild(healthLabel)
}
/*
Called before each frame is rendered
*/
override func update(currentTime: CFTimeInterval) {
healthLabel?.text="Health: \(health)"
if(health <= 3){
healthLabel?.fontColor = SKColor.redColor()
}
now = NSDate()
if (currentNumberOfShips < maxNumberOfShips &&
now?.timeIntervalSince1970 > nextTime?.timeIntervalSince1970 &&
health > 0){
nextTime = now?.dateByAddingTimeInterval(NSTimeInterval(timeBetweenShips!))
var newX = Int(arc4random()%1024)
var newY = Int(self.frame.height+10)
var p = CGPoint(x:newX,y:newY)
var destination = CGPoint(x:newX, y:0.0)
createShip(p, destination: destination)
moverSpeed = moverSpeed/moveFactor
timeBetweenShips = timeBetweenShips!/moveFactor
}
checkIfShipsReachTheBottom()
checkIfGameIsOver()
}
/*
Creates a ship
Rotates it 90º
Adds a mover to it go downwards
Adds the ship to the scene
*/
func createShip(p:CGPoint, destination:CGPoint) {
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.name = "Destroyable"
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = p
let duration = NSTimeInterval(moverSpeed)
let action = SKAction.moveTo(destination, duration: duration)
sprite.runAction(SKAction.repeatActionForever(action))
let rotationAction = SKAction.rotateToAngle(CGFloat(3.142), duration: 0)
sprite.runAction(SKAction.repeatAction(rotationAction, count: 0))
currentNumberOfShips?+=1
self.addChild(sprite)
}
/*
Called when a touch begins
*/
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = (touch as UITouch).locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
if theName == "Destroyable" {
self.removeChildrenInArray([self.nodeAtPoint(location)])
currentNumberOfShips?-=1
score+=1
}
}
if (gameOver?==true){
initializeValues()
}
}
}
/*
Check if the game is over by looking at our health
Shows game over screen if needed
*/
func checkIfGameIsOver(){
if (health <= 0 && gameOver == false){
self.removeAllChildren()
showGameOverScreen()
gameOver = true
}
}
/*
Checks if an enemy ship reaches the bottom of our screen
*/
func checkIfShipsReachTheBottom(){
for child in self.children {
if(child.position.y == 0){
self.removeChildrenInArray([child])
currentNumberOfShips?-=1
health -= 1
}
}
}
/*
Displays the actual game over screen
*/
func showGameOverScreen(){
gameOverLabel = SKLabelNode(fontNamed:"System")
gameOverLabel?.text = "Game Over! Score: \(score)"
gameOverLabel?.fontColor = SKColor.redColor()
gameOverLabel?.fontSize = 65;
gameOverLabel?.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
self.addChild(gameOverLabel)
}
}
Top level variables
We declare a lot of variables at the top of our code. Lets take a look at each one.
var score = 0
var health = 5
One thing I discovered, is if you set the text of a label to be the value of an optional variable it will literally display “(Optional)” on the screen. Since we are displaying these values, I am not making them optional. I’d love if someone could describe a work around for this use case.
var gameOver : Bool?
This variable will be set it to true if the game is over and we are showing the score screen. It will be false while we are playing the game.
let maxNumberOfShips = 10
var currentNumberOfShips : Int?
To ensure this, we will create a variable named currentNumberOfShips which will keep track of how many ships are currently on the screen.
var timeBetweenShips : Double?
var moverSpeed = 5.0
let moveFactor = 1.05
moverSpeed is how long it takes for our ship to fly to the bottom of the screen.
After each plane spawns, we will divide timeBetweenShips and moverSpeed by our moveFactor. I came up with this number through experimentation, it just felt right :)
var now : NSDate?
var nextTime : NSDate?
var gameOverLabel : SKLabelNode?
var healthLabel : SKLabelNode?
The gameOverLabel will be shown when the game is over.
The healthLabel will show our health in the lower left hand corner of the screen.
didMoveToView
This is an overridden function from SKScene that acts as our entry point into our game.
Simply put, this code gets called first.
More specifically, this function is fired immediately after a view is presented. If we had more than one view, they would be fired in whatever order the views were shown.
We use this to call our initializeValues functions.
override func didMoveToView(view: SKView) {
initializeValues()
}
initializeValues
There is a principle in programming called DRY or Don’t Repeat Yourself.
I was initially initializing my variables in 3 places, so to DRY up my code I created this function.
func initializeValues(){
self.removeAllChildren()
score = 0
gameOver = false
currentNumberOfShips = 0
timeBetweenShips = 1.0
moverSpeed = 5.0
health = 5
nextTime = NSDate()
now = NSDate()
It is worth noting that moverSpeed and timeBetweenShips is in seconds.
healthLabel = SKLabelNode(fontNamed:"System")
healthLabel?.text = "Health: \(health)"
healthLabel?.fontSize = 30
healthLabel?.fontColor = SKColor.blackColor()
healthLabel?.position = CGPoint(x:CGRectGetMinX(self.frame) + 80, y:(CGRectGetMinY(self.frame) + 100));
We set up which font to use, what the text should say, what font size to use, and what color our font should be.
The last line sets the position of our text. Notice that I’m using CGRectGetMinX to find the left side of the screen, and I’m using CGRectGetMinY to find the bottom. The values 80 and 100 are just used as offsets so our label is not hidden off of the screen.
self.addChild(healthLabel)
}
Lets talk about that function next.
update
This is another function overridden by SKScene. update gets called for every frame we draw. If we are running our game at 60 frames per second, this function is being called 60 times during that second.
It is worth considering any performance impact that might occur for any code you place here.
override func update(currentTime: CFTimeInterval) {
healthLabel?.text="Health: \(health)"
if(health <= 3){
healthLabel?.fontColor = SKColor.redColor()
}
I also added some fun code that will change the color of our healthLabel from black to red if the players health drops to 3. Its a good way to call attention to the user subtly and tells them to be careful!.
Next, lets check to see if we can create a ship!
now = NSDate()
if (currentNumberOfShips < maxNumberOfShips &&
now?.timeIntervalSince1970 > nextTime?.timeIntervalSince1970 &&
health > 0){
nextTime = now?.dateByAddingTimeInterval(NSTimeInterval(timeBetweenShips!))
The first thing we do is specify when the next ship can be made. We do this by adding a some time to our now value and assigning it to nextTime.
var newX = Int(arc4random()%1024)
var newY = Int(self.frame.height+10)
var p = CGPoint(x:newX,y:newY)
var destination = CGPoint(x:newX, y:0.0)
I had a difficult time trying to pass self.frame.width to arc4random, so I moved on. If anyone has any ideas, please let me know :)
The Y value for our spawn point is the self.frame height + 10. The 10 part is to provide it some buffer room.
The destination will have the same x value as our spawn point, but the y value is simply 0.
createShip(p, destination: destination)
moverSpeed = moverSpeed/moveFactor
timeBetweenShips = timeBetweenShips!/moveFactor
}
We also divide our moverSpeed and timeBetweenShips by moveFactor, which will make our ships spawn faster and faster!
checkIfShipsReachTheBottom()
checkIfGameIsOver()
}
createShip
func createShip(p:CGPoint, destination:CGPoint) {
let sprite = SKSpriteNode(imageNamed:"Spaceship")
sprite.name = "Destroyable"
sprite.xScale = 0.5
sprite.yScale = 0.5
sprite.position = p
Next, we give the sprite some values. I’m specifying its name value as “Destroyable” to identify it as a destroyable object. This part will make more sense in our touchesBegan() function.
let duration = NSTimeInterval(moverSpeed)
let action = SKAction.moveTo(destination, duration: duration)
sprite.runAction(SKAction.repeatActionForever(action))
We tell our sprite to run this action, and to repeat forever.
let rotationAction = SKAction.rotateToAngle(CGFloat(3.142), duration: 0)
sprite.runAction(SKAction.repeatAction(rotationAction, count: 0))
To fix this, I create another SKAction but this time I use rotateToAngle. The first value it accepts is a radian value. I know that I want to rotate the ship 180º, but I wasn’t sure what the radian value for it. To be honest, I asked Google.
It is interesting that this is the value of Pi. I might need to revisit my grade school Geometry books again :)
It is also worth noting that I wanted this rotation to happen instantly, so I set the duration to 0 and the amount of times to repeat the action to 0.
currentNumberOfShips?+=1
self.addChild(sprite)
}
touchesBegan
touchesBegan is another overloaded method from SKScene. It is called whenever touching happens in our scene. We already know what we want this method to do. We want it to destroy ships we touch or restart our game.
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = (touch as UITouch).locationInNode(self)
if let theName = self.nodeAtPoint(location).name {
Notice how we have put our let statement in an if. This will allow us to avoid any problems we might have if no nodes exist at this point. The condition is only met if we can get a node and a name value from the touched location.
if theName == "Destroyable" {
self.removeChildrenInArray([self.nodeAtPoint(location)])
currentNumberOfShips?-=1
score+=1
}
}
We pass removeChildInArray an array that contains only one node in it, however we could have built an array and sent a single command after we iterated through all the touches. I encourage you to try this as an exercise.
Since we are destroying a ship, we decrease currentNumberOfShips and increase our score!
if (gameOver?==true){
initializeValues()
}
}
}
checkIfGameIsOver
If you remember, this function is called every time update() is called.
func checkIfGameIsOver(){
if (health <= 0 && gameOver == false){
self.removeAllChildren()
showGameOverScreen()
gameOver = true
}
}
If it is, we remove everything in the scene, display the game over screen, and set game over to true.
checkIfShipsReachTheBottom
This function is also called every time update() is called.
func checkIfShipsReachTheBottom(){
for child in self.children {
if(child.position.y == 0){
self.removeChildrenInArray([child])
currentNumberOfShips?-=1
health -= 1
}
}
}
Another thing we could do here is check if “Destroyable” is the name of the child. I encourage you to try this.
showGameOverScreen
Finally, this function will display our game over screen.
func showGameOverScreen(){
gameOverLabel = SKLabelNode(fontNamed:"System")
gameOverLabel?.text = "Game Over! Score: \(score)"
gameOverLabel?.fontColor = SKColor.redColor()
gameOverLabel?.fontSize = 65;
gameOverLabel?.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));
self.addChild(gameOverLabel)
}
Conclusion
Sweeet! We’ve written our first game in Swift. At least, it was MY first game with Swift. The game is a little boring, and ends rather quickly, however we know know how to set up 2D sprite games, and how to click and interact with the objects inside of them.
There are many things we could do to make this game better.
Sound. Everything is better with sound effects and music.
Common game design convention states that if we give the user a button, we should give them a reason to not press it. Our game does not support this yet. For homework, we could try decrementing the score every time a ship is missed.
Instead of going straight down, our ships could fly diagonally or follow curves like sine waves. This would be much more difficult and probably more fun.
We could put an actual battleship at the bottom of the screen and give the player a sense of identity.
We could add explosions and various other game juice. You would be surprised what some subtle screen shake and particle engines can add to a relatively boring game.
Anyways, I hope you enjoyed this tutorial. If you have any suggestions for future tutorials or just advice how to make my code better, please feel free to leave a comment. Thanks for you time, and enjoy learning Swift!