Learning Swift — Day 152 to 154

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:

  1. Initialise and store an SKTexture from an image named “player-1”.
  2. Initialise an SKSpriteNode from that texture and assign it to the player property.
  3. Set the player’s zPosition to 10 (quite in the foreground) and its position to 1/6 of the frame’s width and 3/4 of the frame’s height (remember SpriteKit cartesian axis work as they should!).
  4. Add the player as a child to the scene.
  5. Create two new textures from the images names “player-2” and “player-3”.
  6. Create an animation via the SKAction.animate(with:timePerFrame:) method and pass an array of our three textures and 0.01 seconds as arguments.
  7. 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:

  1. Initialise a new SKSpriteNode(color:size:), call it “topSky”, set its color parameter to a UIColor 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.
  2. Change its anchor point to be in the middle top of its frame.
  3. 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.
  4. Set the top sky’s position to the frame’s midX and frame’s height coordinates.
  5. Set the bottom sky’s position to the frame’s midX and to its own frame’s height as Y position.
  6. Add both sprites to the parent node
  7. Change both sprites zPosition to -40.

Write a new createBackground method, performing this:

  1. Initialise a new SKTexture from the image named “background” and call it backgroundTexture.
  2. Loop from 0 to 1 with a for-in loop.
  3. Inside the loop create a new sprite node from our texture.
  4. Set its z-position to -30
  5. Set its anchor point to CGPoint.zero (that should be the bottom left corner of the device)
  6. Set its position to a CGPoint with, as x value, the background texture’s size’s width multiplied by the CGFloat version of the looping index minus the CGFloat version of the looping index times 1 and, as y value, 100.
  7. 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:

  1. A moveLeft SpriteKit action called moveBy(x:y:) with the negative version of the background texture’s size’s width as x and 0 as y and a duration of 20 (seconds).
  2. A moveReset moveBy action with the positive version of the texture’s width and a duration of 0.
  3. A sequence action with the two actions together in an array.
  4. A move forever repeatForever action.
  5. 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):

  1. 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
  2. Set the sprite node’s position to a CGPoint with x equal to the half of the width of the ground texture’s size plus its width multiplied by the loop’s index and y half the texture’s size’s height.
  3. 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:

  1. Create a new SKTexture from the image named “rock” and create a new SKSpriteNode from that texture.
  2. 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.
  3. Create a new sprite node bottomRock from the same texture then set both their zPosition to -20.
  4. Create a new rockCollision sprite node with red color and a size of 32 times the frame’s height and name it “scoreDetect”.
  5. Add all these sprite nodes to the parent node.
  6. Declare an xPosition constant equal to the sum of the frame’s width and the width of the top rock’s frame.
  7. Declare a max CGFloat constant equal to one third of the frame’s height.
  8. Declare a yPosition constant equal to a random CGFLoat value between -50 and the max constant.
  9. Declare a rockDistance constant equal to the CGFLoat version of the number 70.
  10. 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)
  11. Set the bottom rock’s position to a CGPoint of coordinates (xPosition, yPosition minus the rock distance).
  12. 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).
  13. Declare a new constant equal to the frame’s width plus double the width of the top rock’s frame.
  14. Create an SKAction.moveBy(x:y:), with minus end position and 0 as coordinates and 6.2 seconds as duration.
  15. Create a sequence with this move action and remove from parent.
  16. Run the move sequence on both rocks and the rock collision.

Write the startRocks() method which will:

  1. Run the createRocks() method.
  2. Create a wait action for 3 seconds.
  3. Create a sequence of the create and wait actions.
  4. Create a repeat forever action passing in this sequence
  5. 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:

  1. Initialises the score label with the font named “Optima-ExtraBlack” and sets its font size to 24.
  2. Sets the label’s position to the midX of the frame and to its midY minus 60 points.
  3. Sets the labe’s text to “SCORE = 0” to trigger the property observer.
  4. 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 overlapping contactTestBitMask values are in contact with each other in a physics world. To receive contact messages, you set the contactDelegate property of a SKPhysicsWorld 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(_:) or didEnd(_:) and make changes in response to that flag in the update(_:for:) method in a SKSceneDelegate.

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:

  1. 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.
  2. It creates a new .playSoundFileNamed action with the “coin.wav” audio file and with the second parameter set to false. It then runs this action.
  3. It increments the score by 1 and returns from the method.
  4. 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.
  5. 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.
  6. Always in this if statement, a new sound action is created, this time from the “explosion.wav” file and it is run.
  7. 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 using AVFoundation, 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 the AVAudio3DMixing protocol. A scene’s audioEngine property allows overall control of volume and playback.

By default, SKAudioNode objects are positional, i.e. their isPositional property 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: 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:

  1. Set the logo property to be a new SKSpriteNode from the image named “logo”
  2. Set its position to the middle of the frame and add it as a child to the parent node.
  3. Repeat the same for the gameOver property and the “gameover” image.
  4. 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:

  1. Switch over the gameState property and…
  2. …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.
  3. Create a sequence with these actions and run it on the logo.
  4. In case we are .playing: cut-paste here the original two lines of code we had before.
  5. 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.

  1. 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…)
  2. 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).
  3. 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).
  4. 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!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: