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
SKTexturefrom an image named “player-1”. - Initialise an
SKSpriteNodefrom that texture and assign it to theplayerproperty. - Set the player’s
zPositionto 10 (quite in the foreground) and itspositionto 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 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 itscolorparameter to aUIColorwith 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
zPositionto-40.
Write a new createBackground method, performing this:
- Initialise a new
SKTexturefrom the image named “background” and call itbackgroundTexture. - Loop from 0 to 1 with a
for-inloop. - 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
CGPointwith, asxvalue, the background texture’s size’s width multiplied by theCGFloatversion of the looping index minus theCGFloatversion of the looping index times 1 and, asyvalue,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
moveLeftSpriteKit action calledmoveBy(x:y:)with the negative version of the background texture’s size’s width asxand 0 asyand a duration of 20 (seconds). - A
moveResetmoveByaction 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
repeatForeveraction. - 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
CGPointwithxequal to the half of the width of the ground texture’s size plus its width multiplied by the loop’s index andyhalf 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
SKTexturefrom the image named “rock” and create a newSKSpriteNodefrom 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 theirzPositionto-20. - 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
maxCGFloatconstant equal to one third of the frame’s height. - Declare a
yPositionconstant equal to a random CGFLoat value between -50 and themaxconstant. - Declare a
rockDistanceconstant equal to theCGFLoatversion of the number 70. - Set the top rock’s position to a
CGPointof coordinates (xPosition,yPositionplus the height of the top rocks’s size plus the rock distance constant) - Set the bottom rock’s position to a
CGPointof coordinates (xPosition,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.
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
SKPhysicsContactDelegateprotocol can respond when two physics bodies with overlappingcontactTestBitMaskvalues are in contact with each other in a physics world. To receive contact messages, you set thecontactDelegateproperty of aSKPhysicsWorldobject. 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
.playSoundFileNamedaction 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
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:
Summary
A node that plays audio.
Declaration
Discussion
A
SKAudioNodeobject 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
SKSceneobject mixes the audio from nodes in the scene based on parameters defined in theAVAudio3DMixingprotocol. A scene’saudioEngineproperty allows overall control of volume and playback.By default,
SKAudioNodeobjects are positional, i.e. theirisPositionalproperty 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
logoproperty to be a newSKSpriteNodefrom 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 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
gameStateproperty and… - …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
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!
