Update note: This tutorial was updated for Swift and iOS 8 by Caroline Begbie. Original series by Tutorial Team member Marin Todorov.
Are you a seasoned iOS developer who is thinking of creating an iOS game, but have no experience programming games? Maybe learning OpenGL ES makes you uncomfortable, or maybe you’ve just never crossed paths with Cocos2d for iPhone?
Well, good news! UIKit is a power tool for game developers. Its high-end frameworks provide you with the means to create all of your game logic, from creating animations (including frame-animation) to playing audio and video.
This 3-part tutorial series will guide you through the process of building a board game for iPad, where you create words with letters. You’ll also learn about best practices for creating solid, modular iOS apps. And as a bonus, you’ll get a crash course in audio-visual effects with UIKit!
This tutorial series is designed for readers who are already somewhat familiar with Swift, and thus won’t go over details like primitive types or syntax. If your knowledge of Swift is lacking, I recommend checking out our Swift Video Series.
Click through to get your game on!
Getting Started: Introducing Anagrams
The letter / word game with UIKit you will be making in this tutorial is about anagrams.
An anagram is a word or phrase formed by rearranging the letters of another word or phrase. For example, the word cinema can be rearranged into the word iceman. Your game will challenge the player to create anagrams from words and phrases you provide.
The finished game will look something like this:
As you can see, there is a row of letter tiles on the lower end of the screen that makes up the initial phrase. In the screenshot above it is the word admirer, minus the M tile, which the player has dragged to the upper row.
The player’s job is to drag the letters from admirer to form a different word, using the same letters.
Can you guess what it is? I’ve already given you a hint… it starts with an M!
So anyway, this is the game you will be making. As you build this game, you will learn about the following foundational topics:
- Proper model-view-controller (MVC) game structure.
- Loading levels from configuration files.
- Organizing your game controller logic.
- Writing a simple audio controller for UIKit with AV Foundation.
- Using QuartzCore for static visual effects.
- Using UIAnimation to smooth in-game movements.
- Using UIKit particles to add visual effects.
- Building separate layers for the heads-up display (HUD) and the gameplay.
- Using custom fonts to improve the look and polish.
- Upgrading default UIKit components to game-like components.
- Also included: discussion of the software tools I use along the way when I’m creating a game. :]
Oh, and there will be explosions. :] Let’s dig in!
The Starter Project
To get you straight to the good stuff – using UIKit to build an iOS game – this tutorial provides a ZIP file that includes a starter XCode project and all game assets (images, fonts, audio and config files).
Download the Anagrams Part 1 Starter, unzip it and open it up in Xcode.
Have a look at what’s bundled in the starter project. The project file browser on the left should look something like this:
Here’s a quick summary of what you see:
- Config.swift: Basic top-level configuration.
- Levels: Folder containing three .plist files to define the game’s three difficulty levels.
- Classes/Models: Folder for your data models.
- Classes/Views: Folder for your custom view classes.
- Classes/Controllers: Folder for your controllers.
- Assets/Images: Images you’ll use to create the game.
- Assets/Audio: Creative Commons-licensed audio files.
- Assets/Particles: Contains a single PNG file to use for particle effects.
- Assets/Fonts: A custom TTF font for the game HUD.
- Main.storyboard: Open up the storyboard and you’ll see there’s only one screen with the background set to a faint woodgrain image, which is included in the project assets.
- Credits.txt: Includes links to the sources of all the assets used in the project.
Poke around, play the sounds and browse the images. Then run the project.
It’s much better than an empty app window, isn’t it?
In this first part of the series, your goal is to display the initial anagram and target boxes on the screen. In the next parts of this series, you’ll add the ability to drag the tiles and the gameplay logic.
There’s a lot involved to get this first part working. But don’t worry, you’ll break down this epic task into 8 small steps:
- Load the level config file.
- Create a game controller class.
- Create an onscreen view.
- Create a tile view.
- Deal the tiles on the screen.
- Add the letters.
- Slightly rotate the letters randomly.
- Add the targets.
Let’s go over these one step at a time.
1) Load the Level Config File
Select the level1.plist file in the project file browser. Have a look what’s inside:
There are three top-level keys:
- pointsPerTile: the number of points awarded for each correctly-placed tile.
- timeToSolve: The number of seconds the player will have to solve the puzzle.
- anagrams: A list of anagrams. Each anagram contains two items: an initial and a final phrase.
Don’t bother for the moment with the first two and focus on anagrams. It’s an array of array-elements and each element has two string elements. You’re now going to build a data model to load this configuration in Swift.
Right-click (or control-click) on the Anagrams/Classes/Models folder in the project and select New File… to create a new file. Select the iOS\Source\Swift File template and click Next. Name the file Level.swift and click Create.
Open up Level.swift and add the following struct definition:
struct Level { let pointsPerTile: Int let timeToSolve: Int let anagrams: [NSArray] } |
The properties match the structure of the .plist file, so you are simply going to load the data and populate the properties.
Next, add the code to initialize the Level
data model inside the struct definition:
init(levelNumber: Int) { //1 find .plist file for this level let fileName = "level\(levelNumber).plist" let levelPath = "\(NSBundle.mainBundle().resourcePath!)/\(fileName)" //2 load .plist file let levelDictionary: NSDictionary? = NSDictionary(contentsOfFile: levelPath) //3 validation assert(levelDictionary != nil, "Level configuration file not found") //4 initialize the object from the dictionary self.pointsPerTile = levelDictionary!["pointsPerTile"] as Int self.timeToSolve = levelDictionary!["timeToSolve"] as Int self.anagrams = levelDictionary!["anagrams"] as [NSArray] } |
Given a given difficulty level ranging from 1 to 3, the initializer will set up the struct and its properties. There are four small sections in the initializer body:
- Determine the path to the appropriate level config file (level1.plist, level2.plist, etc), based on the
levelNumber
value passed into the method and the bundle path. - Load the file’s contents into an
NSDictionary
, explicitly defined as optional since the file may not exist if you pass in an invalid level number. Fortunately, theNSDictionary
class knows how to parse .plist files! - Validate that there’s something loaded in
levelDictionary
. I useassert
often for errors that should never happen in a shipped app; if they EVER happen during development I want the app to stop and crash, so I can fix it before doing anything else. - Copy the values from the
NSDictionary
to theLevel
instance’s properties.
Open up ViewController.swift and at the end of viewDidLoad()
, add:
let level1 = Level(levelNumber: 1) println("anagrams: \(level1.anagrams)") |
Build and run, and in the Xcode console you will see the list of anagrams for Level 1. Nice!
Now you can remove the println()
line from viewDidLoad()
to keep the console tidy.
That’s one task knocked down. Ready to move ahead to the next one?
2) Create a Game Controller Class
People who are not experienced with the model-view-controller (MVC) paradigm before jumping into iOS development often confuse a controller and a UIViewController
.
- A controller is simply a class that focuses on some part of your app’s logic, separate from your app’s data and the classes used to display that data.
- A class that subclasses
UIViewController
is a controller managing the presentation of a particularUIView
. (AndUIView
s are used to display something on the screen.) You can read up more on the MVC pattern here.
For this tutorial, you are going to create a controller class to manage your game’s logic. Let’s try this out.
Create a new Swift file in the Anagrams/Classes/Controllers folder. Call the new file GameController
.
Open GameController.swift and add the following:
import UIKit class GameController { var gameView: UIView! var level: Level! init() { } func dealRandomAnagram() { } } |
I bet you’ve already spotted the building blocks of your current goal:
gameView
: The view you’ll use to display game elements on the screen.level
: TheLevel
object that stores the anagrams and other settings for the current game level.dealRandomAnagram()
: The method you’ll call to display the current anagram on the screen.
The variables are declared as implicitly unwrapped optionals, as they should never be nil, but you will not be creating the instances in the GameController’s initializer.
The code so far is pretty simple. Change dealRandomAnagram()
to have this placeholder implementation:
func dealRandomAnagram () { //1 assert(level.anagrams.count > 0, "no level loaded") //2 let randomIndex = randomNumber(minX:0, maxX:UInt32(level.anagrams.count-1)) let anagramPair = level.anagrams[randomIndex] //3 let anagram1 = anagramPair[0] as String let anagram2 = anagramPair[1] as String //4 let anagram1length = countElements(anagram1) let anagram2length = countElements(anagram2) //5 println("phrase1[\(anagram1length)]: \(anagram1)") println("phrase2[\(anagram2length)]: \(anagram2)") } |
This method will fetch a random anagram, deal the letter tiles, and create the targets. Here’s what happening in the code, section by section:
- You check to make sure this method is only called after the
level
property is set and that itsLevel
object contains anagrams. - You generate a random index into the anagram list, then grab the anagram at this index. This
randomNumber()
function is defined in Config.swift. - You store the two phrases into
anagram1
andanagram2
. - Then you store the number of characters in each phrase into
anagram1length
andanagram2length
. Even though the letters are the same between anagram1 and anagram2, there can be a different number of spaces so the overall number of characters can be different. - Finally, you print the phrases to the console. This will suffice for testing.
Your app is already set up to load the storyboard Main.storyboard at start up. This storyboard creates a single screen using the ViewController
class.
Since your game will only have one screen, you will add all initialization inside the ViewController
initializer. For games that include more than one view controller, you may want to do your initialization someplace else, such as the AppDelegate
class.
Open up ViewController.swift and add this property inside the ViewController class:
private var controller:GameController |
Finally, add a new initializer to the class:
required init(coder aDecoder: NSCoder) { controller = GameController() super.init(coder: aDecoder) } |
The app will use the init(coder:)
initializer automatically when it instantiates a ViewController
from the storyboard.
There it is! You’ve created a GameController
class and you have an instance of it in your ViewController
class, ready to go off and spill some letter tiles on the screen at any moment. :]
3) Create an Onscreen View
The next step is also quite easy: you need to create a view on the screen and connect your GameController
to it.
Open up Config.swift and look at the first two constants inside the file: ScreenWidth
and ScreenHeight
. You are going to use these constants to create your game view.
Switch back to ViewController.swift and add the following code at the end of viewDidLoad()
:
//add one layer for all game elements let gameView = UIView(frame: CGRectMake(0, 0, ScreenWidth, ScreenHeight)) self.view.addSubview(gameView) controller.gameView = gameView |
Here, you create a new view with the dimensions of the screen and add it to the ViewController
‘s view. Then you assign it to the gameView
property of your GameController
instance. This way the GameController
will use this view for all game elements. You’ll take care of the separate view for the heads-up display (HUD) later.
Having now created your model and controller classes, you’ve come to the point where you need your first custom view class for the Anagrams game to display the tiles.
4) Create a Tile View
In this section, you are going to build a view to show tiles like these:
For now, you’ll focus on just creating the tile squares with the background image. Later, you’ll add the letters on top.
Inside the file group Anagrams/Classes/Views, create a new file called TileView
. Add the following to TileView.swift:
import UIKit //1 class TileView:UIImageView { //2 var letter: Character //3 var isMatched: Bool = false //4 this should never be called required init(coder aDecoder:NSCoder) { fatalError("use init(letter:, sideLength:") } //5 create a new tile for a given letter init(letter:Character, sideLength:CGFloat) { self.letter = letter //the tile background let image = UIImage(named: "tile")! //superclass initializer //references to superview's "self" must take place after super.init super.init(image:image) //6 resize the tile let scale = sideLength / image.size.width self.frame = CGRect(x: 0, y: 0, width: image.size.width * scale, height: image.size.height * scale) //more initialization here } } |
There are a few different sections in the code:
- Make the class a subclass of
UIImageView
. TheUIImageView
class provides you with the means to show an image, so you’ll only need to add a label on top of the image later on. letter
: A property that will hold the letter assigned to the tile.isMatched
: A property, initially set to false, that will hold a Boolean indicating whether this tile has already been successfully matched.- This is
UIImageView
‘s required initializer, which must be overridden even though you’ll never use it. init(letter:sideLength:)
: a custom initializer to set up an instance of the class with a given letter and tile size. It saves the letter to the tile’s variable and loads the tile.png image.- You then calculate by how much you need to scale the default tile so that you get a size that will fit your board for the given word or phrase. To do that, you just adjust the
frame
of theTileView
.UIImageView
resizes its image automatically to match its frame size.
Why do you need to set the size of the tile, you might ask?
Look at the these two setups:
The first example shows a short word that requires large tiles to fill the screen. The latter example shows a phrase that requires smaller tiles because it has almost double the tile count. You can see why the tile view needs to be resize-able so that you can show different size tile depending on how many tiles you need on the screen at a time.
Your controller will calculate the phrase length, divide the screen width by the number of characters and decide on a tile size. You’ll implement that soon, but this code is already enough to show tiles containing just the default empty tile.png image.
Now is a good time to implement more functionality of the game controller, so you can at least see some empty tiles lining up on the screen.
5) Dealing the Tiles on the Screen
Switch back to GameController.swift – your dealRandomAnagram()
method already loads the data you need, but it still does not do much. Time to fix that!
Add the following property to the GameController class:
private var tiles: [TileView] = [] |
This is an array to hold the TileView
objects that will display the tiles at the bottom of the screen. These tiles contain the initial phrase of the anagram.
Later you will also create an array to hold the target views that will display the spaces at the top of the screen where the player has to drag the tiles to form the second phrase of the anagram.
Before you continue, you also need a constant for the space between the tiles. Jump over to Config.swift and add the following:
let TileMargin: CGFloat = 20.0 |
Some of you might already be protesting, “Can’t I just use 20 where I need it and be done?” It’s a good point, but there is a reason for predefining in the config file.
My experience shows that you will have to tweak any numbers you have in your code when it comes time to fine-tune the gameplay. So the best practice is to externalize all numbers to a config file and give those variables meaningful names. This will pay off especially well if you are working in a team and you need to hand the game to a game designer for fine-tuning. :]
OK, back in GameController.swift, get ready to add some code to the end of dealRandomAnagram()
. You have the number of characters in both phrases, so it’s easy to calculate what size tiles you need. Add this code at the end of the method:
//calculate the tile size let tileSide = ceil(ScreenWidth * 0.9 / CGFloat(max(anagram1length, anagram2length))) - TileMargin |
You take 90% of the width of the screen and divide it by the number of characters. You use the length of the longer phrase – remember, the number of characters can vary! You decrease the result by TileMargin
to account for spacing between tiles.
Next find the initial x position of the first tile. It will depend on the length of the word and the tile size you just calculated. Once again, add the following at the end of dealRandomAnagram()
:
//get the left margin for first tile var xOffset = (ScreenWidth - CGFloat(max(anagram1length, anagram2length)) * (tileSide + TileMargin)) / 2.0 //adjust for tile center (instead of the tile's origin) xOffset += tileSide / 2.0 |
You take the screen width, subtract the calculated width of the word tiles and find where the first tile should be positioned on the screen. Wouldn’t it be nice if there were a simpler way to “center” the tiles!
Continuing in the same method, add the following code to display the tiles:
//1 initialize tile list tiles = [] //2 create tiles for (index, letter) in enumerate(anagram1) { //3 if letter != " " { let tile = TileView(letter: letter, sideLength: tileSide) tile.center = CGPointMake(xOffset + CGFloat(index)*(tileSide + TileMargin), ScreenHeight/4*3) //4 gameView.addSubview(tile) tiles.append(tile) } } |
Here’s what’s happening above:
- First you initialize a fresh copy of your
tiles
array. It’s important to start with an empty array here because this function will be called multiple times during a run of your app. Since spaces are skipped, if you didn’t clear out the array then there might be old tiles left over from a previous phrase where there should be spaces. - You then loop through the phrase. You will need both the letter from the phrase and the position of the letter, so use
enumerate
which returns a tuple of the index + the value. - You check each letter and if it is not a space, it’s a go! You create a new tile and position it using
xOffset
,tileSide
andTileMargin
. Here you are calculating the position based on that letter’s position in the phrase, which is represented byindex
. So ifindex
is 5, this math will figure out how much space five tiles take up and add the new tile to the right of that space.
You position the tiles vertically at 3/4ths of the screen height. - Finally, you add the tile to the
gameView
and to thetiles
array. The first actually shows the tile on the screen, while the latter helps you loop faster over the tiles later on.
Alrighty! Time for the final touch – you need to load the level data and call dealRandomAnagram()
. Switch to ViewController.swift and add these two lines at the end of viewDidLoad()
:
controller.level = level1 controller.dealRandomAnagram() |
Remember that you already have a Level
object, so you just pass it on to your GameController
and call dealRandomAnagram()
.
It’s been a long time coming, but build and run to check out the result! Your app should look something like this:
Awesome, it works! It’s alive!!!
Note: Your view probably won’t look exactly like this because the app is choosing a random anagram each time it runs. Run it a few times to see how the app sizes the tiles differently for different phrases. The console output will show you the phrases used.
You are closing in on the finish line for the first part of this tutorial series!
6) Add the Letters
Switch to TileView.swift. This is where you’re going to add letters to those tiles. Find init(letter:sideLength:)
and add the following code to the end:
//add a letter on top let letterLabel = UILabel(frame: self.bounds) letterLabel.textAlignment = NSTextAlignment.Center letterLabel.textColor = UIColor.whiteColor() letterLabel.backgroundColor = UIColor.clearColor() letterLabel.text = String(letter).uppercaseString letterLabel.font = UIFont(name: "Verdana-Bold", size: 78.0*scale) self.addSubview(letterLabel) |
The above code creates a new UILabel
and adds it to the tile view. Because you want the letter to be as easy to read as possible, you create the label to be as large as the tile itself. You then align the letter to show up in the center of the tile.
You ensure the letter is uppercase by setting the label text with String(letter).uppercaseString
. Using capital letters will ease the game play, as they are easier to recognize, specifically by children. Always think about your game’s target audience!
Finally you reuse the scale factor you calculated earlier. For the full-size tile a Verdana font with a size of 78 pts fits well, but for other sizes you need to scale the font size accordingly.
Build and run the project. As I said, you’re now moving fast towards your goal. Good job! :]
Once again, your screen probably won’t look just like this one. But that just means your game controller is doing what’s it’s supposed to do and dealing a random anagram. Nice!
Have another look at the image above. Right now the tiles look like they were put on the board by a pedantic robot: they are perfectly aligned. Now comes the time to add a pinch of randomization!
7) Slightly Rotate the Letters Randomly
Randomization is one of the keys to building a successful computer game. You want to randomize your game as much as possible. For example, no player enjoys enemies that all do the same thing every time the game runs.
In the case of the Anagrams game, you can randomize the placement of each tile so that they look more natural on the game board, as if a human put them down without trying too hard to align them properly:
In order to accomplish this randomization, add the following method to the TileView class in TileView.swift:
func randomize() { //1 //set random rotation of the tile //anywhere between -0.2 and 0.3 radians let rotation = CGFloat(randomNumber(minX:0, maxX:50)) / 100.0 - 0.2 self.transform = CGAffineTransformMakeRotation(rotation) //2 //move randomly upwards let yOffset = CGFloat(randomNumber(minX: 0, maxX: 10) - 10) self.center = CGPointMake(self.center.x, self.center.y + yOffset) } |
This code does two things:
- It generates a random angle between -0.2 and 0.3 radians (using the helper function
randomNumber
defined in Config.swift). Then it callsCGAffineTransformMakeRotation()
to create a new transform for the tile view that will rotate it around its center by that random angle. - It generates a random value between -10 and 0 and moves the tile up by that offset.
Note: You might want to replace all of these numbers with constants you define in Config.swift so you can fine-tune the tile appearance more easily.
Now you just need to call this method. Inside GameController.swift, edit dealRandomAnagram()
by adding the following line of code just after the line that begins with tile.center = ...
:
tile.randomize() |
And that’s all it takes! Hit run again to see the randomized tiles on the board:
Using what you’ve already achieved, you can also quickly add the targets to the game board. Let’s do that before wrapping up this part of the tutorial.
8) Add the Targets
The target views are pretty similar to the tile views. They also subclass UIImageView
and show a default image, and they also are assigned a letter. The difference is that they don’t show a label with that letter and can’t be dragged around. This means the code you need for the targets is similar to what you did before – but simpler!
Inside the file group Anagram/Classes/Views, create a new file called TargetView
. Add the following to TargetView.swift:
import UIKit class TargetView: UIImageView { var letter: Character var isMatched:Bool = false //this should never be called required init(coder aDecoder:NSCoder) { fatalError("use init(letter:, sideLength:") } init(letter:Character, sideLength:CGFloat) { self.letter = letter let image = UIImage(named: "slot")! super.init(image:image) let scale = sideLength / image.size.width self.frame = CGRectMake(0, 0, image.size.width * scale, image.size.height * scale) } } |
Yes, this uses exactly the same properties and function (aside from changing the image) as for the TileView
! You resize the image according to the provided side length and assign the letter – that’s all.
Now to display the targets on the screen. Open up GameController.swift and add the following property to the class:
private var targets:[TargetView] = [] |
Then add the following code inside dealRandomAnagram()
, just before the comment that reads “//1 initialize tile list”:
//initialize target list targets = [] //create targets for (index, letter) in enumerate(anagram2) { if letter != " " { let target = TargetView(letter: letter, sideLength: tileSide) target.center = CGPointMake(xOffset + CGFloat(index)*(tileSide + TileMargin), ScreenHeight/4) gameView.addSubview(target) targets.append(target) } } |
This is almost the same code you used to deal the tiles on the board, except this time you grab the letters from the phrase stored in anagram2
and you don’t randomize their position. You want the target views to look nice and neat, after all!
Why does the target initialization have to come before the tile initialization? Subviews are drawn in the order they are added to their parent view, so adding the target views first ensures that they will appear behind, or below, the tiles.
If you did want to add the target views after the tile views, there are other ways to get them behind the tiles, such as UIView
‘s sendSubviewToBack(view:)
method. But adding them first is the most efficient solution.
Build and run again to see that your board is already close to being finished!
Remember, the anagrams consist of two phrases which have the same letters but might have different spacing. So everything is working as expected if you see something like the screenshot above with three tiles followed by four tiles, while the targets at the top are four tiles followed by three tiles.
Where to Go from Here?
Click here to download the full source code for the project up to this point.
Congrats, you’ve achieved a lot today! Let’s see what you’ve done so far:
- You’ve created a model to load the level configuration.
- You’ve connected the game controller, game view and model.
- You have two custom views, one for tiles and one for targets.
- You’ve finished the initial board setup.
You now have a sound foundation on which to build. Stay tuned for Part 2 of the series, where you’ll begin interacting with the player and building up the gameplay.
In the meantime, if you have any questions or comments about what you’ve done so far, drop by the forums to contribute your two tiles’ worth. :]
How To Make a Letter / Word Game with UIKit and Swift: Part 1/3 is a post from: Ray Wenderlich
The post How To Make a Letter / Word Game with UIKit and Swift: Part 1/3 appeared first on Ray Wenderlich.