Tutorial - vannattd/swift-breakout-game GitHub Wiki
In this tutorial we will be looking at implementing a SpriteKit game on iOS using SpriteKit and Swift. The game we will be implementing is Breakout which highlights two key areas of SpriteKit, physics and collisions. We will be implementing a ball that bounces around the screen and collides with blocks we set up. When the ball collides with these blocks we will want them to disappear. We want the ball to stay in our screen borders and be bounced up by a paddle that we as the user control. If the ball hits the bottom of the screen then the user loses and if the player clears all the blocks, then the user wins. Below is what the final game will look like:
To get started on this application, you will need to have Xcode installed on your Mac. This can be done via the App Store. The current version of Swift, which is 5.3.1 at the time of writing this, is what is used in this tutorial. I will be using an IPhone 8 emulator for this tutorial but any IPhone supporting iOS 14 should work just fine. When you create a new project in Xcode you will want to select Game as the template like below:
Add a name for the project and then keep everything else default. Now you should be all set to begin the tutorial.
- Begin by moving my assets from my git repository to your Assets.xcassets folder (https://github.com/vannattd/swift-breakout-game) or finding your own assets. This provides you with a paddle, block, ball and background for the game.
- Next delete all the default contents in GameScene. This is our main scene for the game and as a result will hold the majority of our games properties and logic. Next, put in two constructors like below:
override init(size: CGSize) {
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
- We can also add some identifiers we will need to distinguish between all the Nodes in our game. You will notice we are using bit identifiers as well as normal Strings. This a SpriteKit standard to compare these identifiers easily and helps when it comes to collision so for collisions we need to keep track of our ball, paddle, blocks and the bottom of the screen.
let ballCategoryName = "ball"
let paddleCategoryName = "paddle"
let blockCategoryName = "block"
let ballCategory:UInt32 = 0x1 << 0
let bottomCategory:UInt32 = 0x1 << 1
let blockCategory:UInt32 = 0x1 << 2
let paddleCategory:UInt32 = 0x1 << 3
- Next let's go into the GameViewController. We will want to delete everything in the viewDidLoad besides the super.viewDidLoad(). We will be implementing the function that better suits our game which is the viewWillLoadSubviews(). We will want to assign the current view as a SkView(Sprite Kit View). If the view doesn’t exist then we will load a game scene to fit the screen size and have an aspect fill for the scale mode. We also will want to show the fps of the game and the node count which are both useful for debugging and development. You viewWillLoadSubviews should look like this:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let skView = self.view as! SKView
if skView.scene == nil{
skView.showsFPS = true
skView.showsNodeCount = true
let gameScene = GameScene(size: skView.bounds.size)
gameScene.scaleMode = .aspectFill
skView.presentScene(gameScene)
}
}
-
Also we will want to make sure our application only starts in landscape mode since this is a landscape app. Go to the top of your file directory and select the app and deselect Portrait Mode as a supported orientation.
-
Now we can dive into a common technique in SpriteKit, the addition of SpriteNodes and Physics. We will load our first sprite node which is the background image. To do this we must first declare our SpriteNode with an image. Luckily we loaded one into our assets already so the code should just be:
let backgroundImage = SKSpriteNode(imageNamed: "bg")
- Now, we want to position this on the screen properly and add it to our GameScene. There are a few ways to center the image properly but I just took the width and height and divided them both by two to get my x and y coordinates respectively.
backgroundImage.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
self.addChild(backgroundImage)
- We also want to declare the physics of our world, to do this we want to set borders so our nodes can’t go off screen. To do this we will want to set up a SKPhysicsBody for the world of our app and turn the gravity off so our ball can fly all about within our borders. The code to do this is:
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
let worldBorder = SKPhysicsBody(edgeLoopFrom: self.frame)
self.physicsBody = worldBorder
self.physicsBody?.friction = 0
A thing to note is that we set this physics body to have a friction of 0. We will talk more about this later in the tutorial.
- Next we will add the ball and the physics for the ball. As you can see we added more physics to this object since it’ll have more properties than our background object. Friction refers to when it crashes into something that it doesn’t slow down. Restitution refers to the “bounciness” of the Node. Allow rotation refers to the object rotating while in motion. Linear damping also refers to resistance, specifically air resistance. One of the key things about the ball that is different is applying an impulse. This is like a push to get the ball moving. If you test your app now, you should have a ball that bounces around the screen.
let ball = SKSpriteNode(imageNamed: "ball")
ball.name = ballCategoryName
ball.position = CGPoint(x: self.frame.size.width/2.8, y: self.frame.size.height/2.8)
self.addChild(ball)
ball.physicsBody = SKPhysicsBody(circleOfRadius: ball.frame.size.width/2)
ball.physicsBody?.friction = 0
ball.physicsBody?.restitution = 1
ball.physicsBody?.linearDamping = 0
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.applyImpulse(CGVector(dx: 2, dy: -2))
- Next, in the same way we did the ball, we can add the paddle. We want a little friction to make the ball manageable after getting hit. We don’t want the paddle to be dynamic since we always want to have the paddle at the bottom of the screen.
let paddle = SKSpriteNode(imageNamed: "paddle")
paddle.name = paddleCategoryName
paddle.position = CGPoint(x: self.frame.midX, y: paddle.frame.size.height * 2)
self.addChild(paddle)
paddle.physicsBody = SKPhysicsBody(rectangleOf: paddle.frame.size)
paddle.physicsBody?.friction = 0.4
paddle.physicsBody?.restitution = 0.1
paddle.physicsBody?.isDynamic = false
- A key thing about the paddle is that we control it, as a result we need to implement touchesBegan, touchesMoved and touchesEnded. These are implemented outside of the init function. We will begin by making a global flag determining whether the paddle is being touched or not and the default will be false. In touchesBegan we want to go through where the touch is. If the touch is on the paddle then we want to set that flag to true. touchesMoved is going to move the paddle based on the current touch position and the previous position only if the paddle is being touched. touchesEnded will set the flag for whether the paddle is being touched to false. The code will look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first! as UITouch
let touchLocation = touch.location(in: self)
let body:SKPhysicsBody? = self.physicsWorld.body(at: touchLocation)
if body?.node?.name == paddleCategoryName{
print("paddle touched")
fingerOnPaddle = true
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if fingerOnPaddle{
let touch = touches.first! as UITouch
let touchLocation = touch.location(in: self)
let prevTouchLocation = touch.previousLocation(in: self)
let paddle = self.childNode(withName: paddleCategoryName) as! SKSpriteNode
var newX = paddle.position.x + (touchLocation.x - prevTouchLocation.x )
newX = max(newX, paddle.size.width/2)
newX = min(newX, self.size.width - paddle.size.width/2)
paddle.position = CGPoint(x: newX, y: paddle.position.y)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
fingerOnPaddle = false
}
- Now we will go back to the init function and add the “bottom” of the game. This will create an invisible Node that expands the very bottom of the screen. We don’t need to add any physics, besides the size ,to this since we just need it to sit at the bottom of the screen.
let bottomRect = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: self.frame.size.width, height: 1)
let bottom = SKNode()
bottom.physicsBody = SKPhysicsBody(edgeLoopFrom: bottomRect)
self.addChild(bottom)
- Next we want to begin working with custom collisions so we will want to add the bit masks for each node and node type. The contact test bitmasks put what the ball node has the ability to interact with.
bottom.physicsBody?.categoryBitMask = bottomCategory
ball.physicsBody?.categoryBitMask = ballCategory
paddle.physicsBody?.categoryBitMask = paddleCategory
ball.physicsBody?.contactTestBitMask = bottomCategory | blockCategory
- Now we will want to implement the SKPhysicsContactDelegate and the didBegin() function. We want the didBegin() function to look at the bodies coming from the contact that calls the didBegin() function. This looks at the bit masks of these bodies. If the bitmasks match up for the ball and bottom then we want the game to lose. If the ball and the block interact then we want the block to be destroyed which we don’t have implemented yet. This function will look like this, there will be some errors due to the fact that some stuff isn’t implemented yet, but we will get it fixed:
func didBegin(_ contact: SKPhysicsContact) {
var firstBody = SKPhysicsBody()
var secondBody = SKPhysicsBody()
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask{
firstBody = contact.bodyA
secondBody = contact.bodyB
}else{
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if firstBody.categoryBitMask == ballCategory && secondBody.categoryBitMask == bottomCategory{
let gameOverScene = GameOverScene(size: self.frame.size, playerWon: false)
self.view?.presentScene(gameOverScene)
}
if firstBody.categoryBitMask == ballCategory && secondBody.categoryBitMask == blockCategory{
secondBody.node?.removeFromParent()
if (isGameWon()){
let youWinScene = GameOverScene(size: self.frame.size, playerWon: true)
self.view?.presentScene(youWinScene)
}
}
}
- Let's start fixing this by creating the blocks on the screen. Like other Nodes we will add it to our view and add physics for the blocks. We will also want to implement the way our blocks are spaced. We can choose how many blocks we want and how many rows. For the sake of this tutorial we will do 3 rows of 6 blocks in each row. This will have 2 for loops and we will calculate the space of each block based on the index of where it is at:
let numRows = 3
let numBlocks = 6
let blockWidth = SKSpriteNode(imageNamed: "block").size.width
let padding:Float = 20
let part1:Float = Float(blockWidth) * Float(numBlocks) + padding * (Float(numBlocks) - 1 )
let part2:Float = (Float(self.frame.size.width)) - part1
let offset:Float = part2 / 2
for row in 1 ... numRows{
var yOffset:CGFloat{
switch row {
case 1:
return self.frame.size.height * 0.8
case 2:
return self.frame.size.height * 0.6
case 3:
return self.frame.size.height * 0.4
default:
return 0
}
}
for index in 1 ... numBlocks{
let block = SKSpriteNode(imageNamed: "block")
let calc1:Float = Float(index) - 0.5
let calc2:Float = Float(index) - 1
block.position = CGPoint(x: CGFloat(calc1 * Float(block.frame.size.width) + calc2 * padding + offset), y: yOffset)
block.physicsBody = SKPhysicsBody(rectangleOf: block.frame.size)
block.physicsBody?.allowsRotation = false
block.physicsBody?.isDynamic = false
block.physicsBody?.friction = 0
block.name = blockCategoryName
block.physicsBody?.categoryBitMask = blockCategory
self.addChild(block)
}
}
- Now that we have the blocks generated, we can create a function to determine whether or not the game is won. The win condition of the game is when all the blocks are destroyed we will win. We can create a function to check this outside of the init function. This will also help fix our collision class. This will look like this:
func isGameWon() -> Bool {
var numBlocks = 0
for nodeObject in self.children{
let node = nodeObject as SKNode
if node.name == blockCategoryName{
numBlocks += 1
}
}
return numBlocks <= 0
}
- To finish up the game we will want to create a win screen. To this we will want to create a new GameScene named GameOverScene. To this create a new Cocoa Touch Class and make it a subclass of SKScene. The default doesn’t import SpriteKit so import it at the top of the file. Like the beginning of our original game scene we will want to create two init functions, one with size and another with a decoder. For the size init function we will also add a Bool parameter to tell if the game was won or lost. These empty functions will look like this:
init(size: CGSize, playerWon: Bool) {
super.init(size: size)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
- Now we can add the logic for the game over screen. We will want to add a background image which we will just use the same image as the main screen. We then can add a SKLabelNode and add the font and font size to display whether the user won or lost based on the Bool passed in. This should look like this:
init(size: CGSize, playerWon: Bool) {
super.init(size: size)
let backgroundImage = SKSpriteNode(imageNamed: "bg")
backgroundImage.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
self.addChild(backgroundImage)
let gameOverLabel = SKLabelNode(fontNamed: "Avenir-Black")
gameOverLabel.fontSize = 46
gameOverLabel.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
if playerWon{
gameOverLabel.text = "YOU WIN!"
}
else{
gameOverLabel.text = "GAME OVER!"
}
self.addChild(gameOverLabel)
}
Our new screen will look like this as a lost:
- To finish up this new scene will add a way to play the game again. We will implement the touchesBegan function to show the main game screen again to allow the user to continue playing. This will look like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let breakOutGameScene = GameScene(size: self.size)
self.view?.presentScene(breakOutGameScene)
}
- That should be it! Run the app and assure that everything is working properly!
As you can see implementing a basic game using SpriteKit isn’t extremely difficult. Implementing the physics and collisions is the major area that our tutorial explores, but SpriteKit has many more interesting areas to dive into. For example, if the collision detection using the bit masks we used isn’t what you want to use, you can just compare each Node's x and y values to determine if a collision occurred. Other areas SpriteKit can dive into is using Metal which is a way to help render your game and gives more control on the backend of your game. We can also write a game using JavaScript and make a web application, mobile app. For more information on SpriteKit you can read the documentation here. You can also find my git repository here for the final code for the tutorial above.