Hacking with Swift — Learning Project 36 Crashy Plane
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:
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
Create a new method called
createPlayer()which does the following things:
- Initialise and store an
SKTexturefrom an image named “player-1”.
- Initialise an
SKSpriteNodefrom that texture and assign it to the
- Set the player’s
zPositionto 10 (quite in the foreground) and its
positionto 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
.repeatForeveraction with our animation and call it on the player.
Now call this method from the
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 its
colorparameter to a
UIColorwith 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
Write a new
createBackground method, performing this:
- Initialise a new
SKTexturefrom the image named “background” and call it
- Loop from 0 to 1 with a
- 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
xvalue, the background texture’s size’s width multiplied by the
CGFloatversion of the looping index minus the
CGFloatversion of the looping index times 1 and, as
- 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:
moveLeftSpriteKit action called
moveBy(x:y:)with the negative version of the background texture’s size’s width as
xand 0 as
yand a duration of 20 (seconds).
moveByaction 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
- 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
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
xequal to the half of the width of the ground texture’s size plus its width multiplied by the loop’s index and
yhalf 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.
createRocks() method, which is so divided:
- Create a new
SKTexturefrom the image named “rock” and create a new
SKSpriteNodefrom 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
bottomRockfrom the same texture then set both their
- Create a new
rockCollisionsprite 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
xPositionconstant equal to the sum of the frame’s width and the width of the top rock’s frame.
- Declare a
CGFloatconstant equal to one third of the frame’s height.
- Declare a
yPositionconstant equal to a random CGFLoat value between -50 and the
- Declare a
rockDistanceconstant equal to the
CGFLoatversion of the number 70.
- Set the top rock’s position to a
CGPointof coordinates (
yPositionplus the height of the top rocks’s size plus the rock distance constant)
- Set the bottom rock’s position to a
CGPointof coordinates (
yPositionminus the rock distance).
- Set the rock collision sprite node position to a
CGPointof coordinates (
xPositionplus 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.
startRocks() method which will:
- Run the
- 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
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
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
Pixel-perfect physics in SpriteKit, plus explosions and more.
Conform to the
SKPhysicsContactDelegate protocol, thus described in the Documentation:
Methods your app can implement to respond when physics bodies come into contact.
An object that implements the
SKPhysicsContactDelegateprotocol can respond when two physics bodies with overlapping
contactTestBitMaskvalues are in contact with each other in a physics world. To receive contact messages, you set the
contactDelegateproperty of a
SKPhysicsWorldobject. The delegate is called when a contact starts or ends.
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
didEnd(_:)and make changes in response to that flag in the
update(_:for:)method in a
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
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).
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…
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.
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
.playSoundFileNamedaction with the “coin.wav” audio file and with the second parameter set to
false. 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
nilotherwise return from the method.
- Now, if either of the two contact body’s nodes are the player, it checks if there is an
SKEmitterNodewith 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
ifstatement, 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:
A node that plays audio.
SKAudioNodeobject is used to add an audio to a scene. The sounds are played automatically using
AVFoundation, and the node can optionally add 3D spatial audio effects to the audio when it is played.
The currently presented
SKSceneobject mixes the audio from nodes in the scene based on parameters defined in the
AVAudio3DMixingprotocol. A scene’s
audioEngineproperty allows overall control of volume and playback.
SKAudioNodeobjects are positional, i.e. their
isPositionalproperty is set to
true. 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:
Create three new properties, a logo and a game over implicitly unwrapped sprite nodes and a game state which will be equal to
Write a new
createLogos() method with these operations inside:
- Set the
logoproperty to be a new
SKSpriteNodefrom 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
gameOverproperty and the “gameover” image.
- Set its
alphato 0 and add it as a child.
Add a call to
didMove(to:) and delete the call to
createPlayer(), set its physics body’s
.isDynamic property to
Modify now the
- Switch over the
- …in case it is in a
.showingLogostate, 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
- In case we are
.playing: cut-paste here the original two lines of code we had before.
- In case we are
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
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
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.
createRocks() change the color of the
rockCollision constant to be
Go to GameViewController.swift and turn all the last three properties of the view to
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
didMove(to:), initialise the
rockPhysics property to be an
SKPhysicsBody with the rock texture as texture and its size as size.
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.
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!