Hacking with Swift — Learning Project 36 Crashy Plane
Setting up
Create a new Xcode project based on the Game template, select SpriteKit as language and call it “Crashy Plane”, then save it somewhere sensible!
We are going back to SpriteKit, aren’t you incredibly excited?!
Creating a player: resizeFill
vs aspectFill
Delete the Actions.sks file and clean the GameScene.swift. Open GameScene.sks, delete the label and change the anchor point to 0.0, 0.0 but do not change the size!
Drag the content of the GFX folder of the provided assets to Xcode’s assets catalogue.
In the Project Navigator, select the project group (yellow folder), right click and choose “New Group”, name it “Content” and hit Enter. Copy there the remaining assets.
In GameScene.swift create a new implicitly unwrapped property called “player” of type SKSpriteNode
.
Create a new method called createPlayer()
which does the following things:
- Initialise and store an
SKTexture
from an image named “player-1”. - Initialise an
SKSpriteNode
from that texture and assign it to theplayer
property. - Set the player’s
zPosition
to 10 (quite in the foreground) and itsposition
to 1/6 of the frame’s width and 3/4 of the frame’s height (remember SpriteKit cartesian axis work as they should!). - Add the player as a child to the scene.
- Create two new textures from the images names “player-2” and “player-3”.
- Create an animation via the
SKAction.animate(with:timePerFrame:)
method and pass an array of our three textures and 0.01 seconds as arguments. - Set a
.repeatForever
action with our animation and call it on the player.
Now call this method from the didMove(to:)
method.
Go to GameScene.sks and change the size to be 375 x 667.
Sky, background and ground: parallax scrolling with SpriteKit
Write a new createSky
method, inside perform the following:
- Initialise a new
SKSpriteNode(color:size:)
, call it “topSky”, set itscolor
parameter to aUIColor
with hue of 0.55, saturation of 0.14, brightness of 0.97 and fully opaque and its size to be the frame’s width and 2/3 its height. - Change its anchor point to be in the middle top of its frame.
- Repeat for a new sprite note called “bottomSky”, with ever so slightly different color parameters (saturation 0.16 and brightness 0.96) and size equal to the frame’s width and 1/3 its height.
- Set the top sky’s position to the frame’s midX and frame’s height coordinates.
- Set the bottom sky’s position to the frame’s midX and to its own frame’s height as Y position.
- Add both sprites to the parent node
- Change both sprites
zPosition
to-40
.
Write a new createBackground
method, performing this:
- Initialise a new
SKTexture
from the image named “background” and call itbackgroundTexture
. - Loop from 0 to 1 with a
for-in
loop. - Inside the loop create a new sprite node from our texture.
- Set its z-position to -30
- Set its anchor point to
CGPoint.zero
(that should be the bottom left corner of the device) - Set its position to a
CGPoint
with, asx
value, the background texture’s size’s width multiplied by theCGFloat
version of the looping index minus theCGFloat
version of the looping index times 1 and, asy
value,100
. - Finally, add the background as a child.
Upon running I see something bluish … are there mountains? Are you kidding me! Always inside the loop, which is not specified but it should become clear once you write the first few lines, add the following code:
- A
moveLeft
SpriteKit action calledmoveBy(x:y:)
with the negative version of the background texture’s size’s width asx
and 0 asy
and a duration of 20 (seconds). - A
moveReset
moveBy
action with the positive version of the texture’s width and a duration of 0. - A sequence action with the two actions together in an array.
- A move forever
repeatForever
action. - A call to this endless running on the background sprite node.
Something is wrong with my code. Let me check as I may have done some typo. Found it: I had written bottomSky.zPosition
twice!
Now go on and write the createGround()
method, which is very similar to the one which created the background (I will highlight the differences):
- Create a new texture from the “ground” image, loop over from 0 to 1, create a sprite node from the texture, set its z-position to -10
- Set the sprite node’s position to a
CGPoint
withx
equal to the half of the width of the ground texture’s size plus its width multiplied by the loop’s index andy
half the texture’s size’s height. - Create the same set of actions, just running on the correct textures and nodes.
Run your app now and it should be working beautifully.
Creating collisions
Write the createRocks()
method, which is so divided:
- Create a new
SKTexture
from the image named “rock” and create a newSKSpriteNode
from that texture. - Set the node’s z-rotation to
.pi
(180°) and its x-Scale to-1
(-100%, horizontally flipped). Don’t know why both these are necessary but fine. - Create a new sprite node
bottomRock
from the same texture then set both theirzPosition
to-20
. - Create a new
rockCollision
sprite node with red color and a size of 32 times the frame’s height and name it “scoreDetect”. - Add all these sprite nodes to the parent node.
- Declare an
xPosition
constant equal to the sum of the frame’s width and the width of the top rock’s frame. - Declare a
max
CGFloat
constant equal to one third of the frame’s height. - Declare a
yPosition
constant equal to a random CGFLoat value between -50 and themax
constant. - Declare a
rockDistance
constant equal to theCGFLoat
version of the number 70. - Set the top rock’s position to a
CGPoint
of coordinates (xPosition
,yPosition
plus the height of the top rocks’s size plus the rock distance constant) - Set the bottom rock’s position to a
CGPoint
of coordinates (xPosition
,yPosition
minus the rock distance). - Set the rock collision sprite node position to a
CGPoint
of coordinates (xPosition
plus double the width of the rock collision’s size, the midY coordinate of the frame). - Declare a new constant equal to the frame’s width plus double the width of the top rock’s frame.
- Create an
SKAction.moveBy(x:y:)
, with minus end position and 0 as coordinates and 6.2 seconds as duration. - Create a sequence with this move action and remove from parent.
- Run the move sequence on both rocks and the rock collision.
Write the startRocks()
method which will:
- Run the
createRocks()
method. - Create a wait action for 3 seconds.
- Create a sequence of the create and wait actions.
- Create a repeat forever action passing in this sequence
- Run the repeat forever action.
Now call the startRocks()
method inside didMove(to:)
.
Add an implicitly unwrapped label node for the score, then an integer score variable initialised to 0 with a didSet
property observer which will change the label’s text to “Score: \(score)”
.
Now write the createScore()
method, which:
- Initialises the score label with the font named “Optima-ExtraBlack” and sets its font size to 24.
- Sets the label’s position to the midX of the frame and to its midY minus 60 points.
- Sets the labe’s text to “SCORE = 0” to trigger the property observer.
- Sets the font’s color to black and add the label to the parent node.
Add this last method to the didMove(to:)
method.
Pixel-perfect physics in SpriteKit, plus explosions and more.
Conform to the SKPhysicsContactDelegate
protocol, thus described in the Documentation:
Summary
Methods your app can implement to respond when physics bodies come into contact.
Declaration
Discussion
An object that implements the
SKPhysicsContactDelegate
protocol can respond when two physics bodies with overlappingcontactTestBitMask
values are in contact with each other in a physics world. To receive contact messages, you set thecontactDelegate
property of aSKPhysicsWorld
object. The delegate is called when a contact starts or ends.Important
The physics contact delegate methods are called during the physics simulation step. During that time, the physics world can’t be modified and the behavior of any changes to the physics bodies in the simulation is undefined. If you need to make such changes, set a flag inside
didBegin(_:)
ordidEnd(_:)
and make changes in response to that flag in theupdate(_:for:)
method in aSKSceneDelegate
.You can use the contact delegate to play a sound or execute game logic, such as increasing a player’s score, when a contact event occurs. The following code shows how to display a shockwave effect when two nodes with the name ball come into contact. The code only creates the effect when the collision impulse is above a specified threshold:
Listing 1 Creating a shockwave effect when objects come into contact
In didMove(to:)
set the physics world’s gravity to have a dy
parameter of -5.0
instead of the usual -9.8
, then set self
as its contact delegate (that is, who needs to be notified of something happening).
Inside createPlayer()
, after the call to addChild()
, set the player’s physics body to a new SKPhysicsBody
with the player texture as the texture and its size as size. Then set the contact test bit mask of the player’s physics body (which needs to be implicitly unwrapped) to be equal to its collision bit mask (which means that the delegate (us) should be notified whenever the player collides with anything). After this set the player’s physics body to be dynamic and its collision bit mask to 0…
Inside createGround()
, this time before the call to addChild()
, set the ground’s physics body to be a new SKPhysicsBody
with the implicitly unwrapped ground’s texture as texture and its size as size. Finally, set the physics body to be non dynamic.
Inside the empty touchesBegan(_:with:)
method set the velocity of the player’s physics body to be a CGVector
with 0 for both dx and dy components and call the applyImpulse()
on it by passing a CGVector
with 0 as dx and 20 as dy. This means that every time we tap on the screen the plane will be given a push upwards.
Insert the previously deleted update(_:)
method and, inside it, declare a new constant to hold 1/1000th of the player’s physics body’s velocity’s dx value and then create a rotate(toAngle:duration:)
action with the value constant and 0.1 seconds as the two arguments. Finally make the player run this action.
Inside the createRocks()
method, after the second line, make the top rock’s physics body be a new SKPhysicsBody
with the rock texture as texture and its size as size, then make it non dynamic. Repeat this procedure for the bottom rock. After the creation of the rock collision rectangle, set its physics body to be a rectangle of the sprite node’s size, then set its physics body to be non dynamic.
Go to GameViewController.swift and, at the end of the if let
statement of the viewDidLoad()
method set the showPhysics
property of the view to true.
Now I would really like to try this app out on my device but, alas, after updating to Xcode 10.3 (which required erasing Xcode 10.2.1 because the fabulous Mac App Store was not seeing the update), I need to wait for Xcode to rebuild the debugger support for my devices from scratch…
I really wonder what is wrong this summer with software updates, no one of them was flawless or brought no problems with it! Come on, devs! Do you want to go on holiday? Fine! Of course! Go! But do not release a rushed update just before going, for God’s sake! Since the middle of June it has been a continuous nightmare!
So, after waiting for Sir Xcode of Nottingham to wake up, I could run the app and it works, it’s nice but … the score label is not where Paul’s image shows. I will change the x-coordinate to be frame.maxX - 100
.
Just below the touchesBegan
method, implement the didBegin(_:)
contact method which performs the following things:
- It checks whether either bodyA or bodyB of the contact event is our score rectangle (you remember? The history of the egg and the chicken? That one!). If so, it checks is the bodyA’s node is the player, in which case it removes bodyB’s node, otherwise it performs the opposite thing.
- It creates a new
.playSoundFileNamed
action with the “coin.wav” audio file and with the second parameter set tofalse
. It then runs this action. - It increments the score by 1 and returns from the method.
- Supposing steps 1.-3. Didn’t happen, it checks that whatever node contained in the body that triggered the contact is not equal to
nil
otherwise return from the method. - Now, if either of the two contact body’s nodes are the player, it checks if there is an
SKEmitterNode
with the file named “PlayerExplosion” and stores it in a constant. It then sets its position on the player’s position and adds it as a child. - Always in this
if
statement, a new sound action is created, this time from the “explosion.wav” file and it is run. - The player is removed from the parent node and the speed is set to 0.
Background music with SKAudioNode
, an intro, plus game over
Add a new backgroundMusic
property of type SKAudioNode!
. This class is thus described in the Documentation:
Summary
A node that plays audio.
Declaration
Discussion
A
SKAudioNode
object is used to add an audio to a scene. The sounds are played automatically usingAVFoundation
, and the node can optionally add 3D spatial audio effects to the audio when it is played.The currently presented
SKScene
object mixes the audio from nodes in the scene based on parameters defined in theAVAudio3DMixing
protocol. A scene’saudioEngine
property allows overall control of volume and playback.By default,
SKAudioNode
objects are positional, i.e. theirisPositional
property is set totrue
. If you add an audio node to a scene with a`listener`
set, SpriteKit will set the stereo balance and the volume based on the relative positions of the two nodes.You can explicitly set the volume or stereo balance to an audio node by running actions on it.
SpriteKit includes actions that reduce an audio node’s volume by changing either its occlusion or obstruction. The difference between these actions is that occlusion affects both the direct and reverb paths of the sound while obstruction only affects the direct path. The change volume action offers absolute control over an audio node’s volume.
You can manually set the stereo balance of an audio node with a stereo pan action.
Special effects, such as speeding up or slowing down audio by changing the playback rate and adding reverb are also available as audio actions.
To learn more about audio actions, see Controlling the Audio of a Node.
At the end of didMove(to:)
conditionally bind the “music.m4a” resource file found in the main application bundle to a musicURL
constant. Inside the braces, set the backgroundMusic
property to an instance of SKAudioNode
taken from the musicURL
. Then add the background music as a child node.
At the top of the file, after the import
line, create a new enumeration called GameState
with three cases: showingLogo
, playing
and dead
.
Create three new properties, a logo and a game over implicitly unwrapped sprite nodes and a game state which will be equal to GameState.showingLogo
.
Write a new createLogos()
method with these operations inside:
- Set the
logo
property to be a newSKSpriteNode
from the image named “logo” - Set its position to the middle of the frame and add it as a child to the parent node.
- Repeat the same for the
gameOver
property and the “gameover” image. - Set its
alpha
to 0 and add it as a child.
Add a call to createLogos()
in didMove(to:)
and delete the call to startRocks()
.
Inside createPlayer()
, set its physics body’s .isDynamic
property to false
.
Modify now the touchesBegan()
method:
- Switch over the
gameState
property and… - …in case it is in a
.showingLogo
state, set it to.playing
, create a fade out action over 0.5 seconds, create a remove from parent action, then a wait for 0.5 seconds action then a run action which will set the player’s physics body to be dynamic and start the rocks movement. - Create a sequence with these actions and run it on the
logo
. - In case we are
.playing
: cut-paste here the original two lines of code we had before. - In case we are
.dead
: break!
Inside didBegin(_:)
add these code before the removal of the node from the parent: set the game over’s alpha value to 1, change the game state to .dead
, stop the background music from playing with the stop()
action.
Now modify the .dead
case by removing the break
statement, conditionally binding the “GameScene” file to a new instance of the GameScene()
class, setting its scale mode to .aspectFill
, set its transition to be a .moveIn
SKTransition
with a right direction and a duration of 1 second and, finally, presenting the scene on the view with this transition.
At the start of the update(_:)
method add a guard statement so that it will check for the player not being nil
or it will return.
Inside createRocks()
change the color of the rockCollision
constant to be .clear
.
Go to GameViewController.swift and turn all the last three properties of the view to false
.
Optimising SpriteKit physics
Add two properties to the GameScene class, one to store the SKTexture
of the image named “rock” and one that is a still implicitly unwrapped SKPhysicsBody
.
Inside didMove(to:)
, initialise the rockPhysics
property to be an SKPhysicsBody
with the rock texture as texture and its size as size.
Inside createRocks()
substitute both rock’s physics bodies with rockPhysics.copy() as? SKPhysicsBody
.
Now create a new SKEmitterNode
property from the file named “PlayerExplosion”.
According to Paul’s writing the tutorial finishes here but … shouldn’t we call this property inside the didBegin(_:)
method? I feel confused … I will trust Paul, though, even if this is not explained.
Wrap up!
This was a nice project and I am happy I have completed it.
There are three challenges proposed, but I am not sure if I feel like facing all of them.
- Create different kinds of obstacles (this could take 2-3 hours or more to create, between finding the graphics, implementing it, changing the code and test…)
- Make the difficulty ramp up ever so slowly, either by decreasing the gap between the rocks or by increasing the world gravity (this could be done in about 30 minutes I think).
- Introduce a secondary scoring mechanism: the player could get extra points if they fly through hoops in between the rocks (too much work for me right now, especially as I know that SpriteKit will most probably not be my call).
- Make it a Universal game, supporting both iPad and iPhone (this is said to be a bigger challenge, and being that I do not see why, I will trust him!)
I think I need a pause with this and will see if I feel like coming back to it at the end of the course, which is now 2 projects away!
Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know of our gratitude.
He has written about 20 great books on Swift, all of which you can check about here (affiliate links so please use them if you want to support both his work .
The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a completely free donation to support my studies.
I will be attending the Pragma Conference in Bologna this fall and I would like to be able to write articles about what I learn but doing that for free means that I will not be able to give it any priority at all.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!