Reflections on Day 83
Yesterday was a crazy day, absolutely but this morning, thanks also to the help of the always present and immensely kind Rob Baldwin (here on Twitter) I was able to find a solution to yesterday’s challenge 2!
Starting from where I had left it yesterday I cancelled everything after the if isAlertShown == false
statement so that there would be no alternative. Then, inside the update
method, I modified the default
case so that it would change the beacon identifier label’s text back to “No beacon detected” and, then, I used GCD to set the Boolean back to false
only after 10 seconds, which seems the delay needed to have the system realise a beacon has disappeared from the radars.
The interesting thing is that as soon as I turn off the beacon the screen turns grey and the text label changes, meaning that the system has detected the beacon to have disappeared.
But never mind, this is really nice now, such a little change and such a great result!
Hacking with Swift – Learning Project 23, episode 1
… success is not final, failure is not fatal: it is the courage to continue that counts (Winston Churchill).
With this quote we start what is one of the longest project in the series! The approach Paul is using (three steps forward, one step back, as he calls it) is for sure very effective but, personally, I would have preferred a 2+1 instead of a 3+1 approach. Sure this, would have taken 150 days instead of 100 but that is probably how realistically it will end for me and for many other beginners!
He hopes that when we finish this we will be able to do something new without thinking twice about it. The first thing I want to do when I finish is to purchase SnippetsLabs and go over all projects we have done and create a library of code where I will store how to do things. Remembering which project had what code could be quite lethal, I suppose.
But let’s get started…
Setting up
This is the usual set-up for any SpriteKit project (cleaning, iPad-only, etc. …). We need to import the audio files and the scene files to the project, just under the Info.plist file and, inside the asset catalogue, the images contained in this repository at the Project23-files folder.
Basics quick start: SKShapeNode
We start off by creating the background for our scene with an SKSpriteNode
with an image named “sliceBackground”; we position it in the center of the screen (CGPoint(x: 512, y: 384)
, set its blendMode
to .replace
(which means that the source colour replaces the destination colour), set its .zPosition
to be -1
(i.e.: behind everything else) and we add it to our main scene.
Next, we set our physics world’s gravity to be a Core Graphic vector with a delta x of 0 (that is, the gravity doesn’t influence the horizontal movement of bodies) and a delta y of -6
, so that the gravity is quite less than the Earth’s one (which is -9.81
). To be precise (and picky as me!) the gravity
property of the physics world class property is defined as a vector that specifies the gravitational acceleration applied to physics bodies in the physics world. The components of this property are measured in meters per second and the default value is (0.0, -9.8)
, which represent’s Earth’s gravity. As a final check, this means that, in our world, an object that falls and that is influenced by Earth’s gravity will fall at a speed of 9.8 m/s, while, in our app, the same object will fall at 6 m/s.
We then set the speed
of our physics world to be 0.85
. This value measures the rate at which the simulation executes and has a default value of 1.0
, which means that the simulation runs at normal speed. A value other than the default changes the rate at which time passes in the physics simulation. For example, a speed value of 2.0
indicates that time in the physics simulation passes twice as fast as the scene’s simulation time. A value of 0.0
, instead, pauses the physics simulation.
At the end of the didMove(to:)
method we call the three methods that we are going to create in a very short while: createScore
, createLives
and createSlices
.
Create score
This is quite simple (because we have already done it many times). We set the game score label to be an SKLabelNode
with a font named “Chalkduster”, set its text’s horizontal alignment mode to be .left
, its font size to 48
and its position to be (8, 8) in the scene. We also set the score property to be 0
and add the child to the scene.
Interesting is the order in which things are done: we first add the child to the scene and then set its position. I wonder if doing the things in reverse order would have changed the final result.
Create lives
The player will have three lives before losing the game so we create a loop from 0 up to and not including 3 where we create a new SKSpriteNode
with an image named “sliceLife”, we set its position to be x: (834 + (i * 70))
and y: 720
. We then add the child to the scene and to the array.
In this occasion we set the position first and added the child afterwards … interesting to say the least.
Create slices
Here I took the freedom to reorganise Paul’s creation code because otherwise my very limited brain would have been quite lost: first of all we create two global variables of type SKShapeNode
called, respectively, “activeSliceBG” (BG for background) and “activeSliceFG” (for foreground). But what is an SKShapeNode
? An SKShapeNode
is a mathematical shape that can be stroked or filled. It allows us to create on-screen graphical elements from mathematical points, lines and curves. The advantage of this over rasterised graphics, such as those displayed by textures, is that shapes are rasterised dynamically at runtime to produce crisp detail and smoother edges (I guess this is enough of an introduction, but if you feel like knowing more I have prepared a playground with some code inside that reproduces—or tries to—Apple’s documentation).
At this point, inside the createSlices
method we can instantiate the two shape nodes which will have, respectively starting from the background one, a .zPosition
of 2
and 3
, a .strokeColor
of UIColor(red: 1, green: 0.9, blue: 0, alpha: 1)
(which should be some kind of yellow) and UIColor.white
and a .lineWidth
of 9
and 5
, so that the second looks like if inside the first and making it glow!
Shaping up for action: CGPath
and UIBezierPath
Touch methods
First thing, we need to implement some touch methods. Some we have already encountered, others we have not. Let’s begin with touchesBegan
, which tells this object that one or more new touches occurred in a view or window. Inside it, we need to check that there are some touches and grab the first one of them, remove all the points from the activeSlicePoints
array (while keeping the capacity, even if I do not yet understand why this is—maybe to avoid the action of reallocating memory?), grab the location of the touch and append it to our now emptied array. We will then call the redrawActiveSlice()
method (not written yet), remove all actions from our two active slice properties (the reason will become clear in just a moment) and set their alpha to 1
so that they are fully visible.
The next method is the touchesMoved
one, which tells the responder when one or more touches associated with an event changed. UIKit calls this method when the location or force of a touch changes. Inside it, we need to check that there are some touches and pick out the first one of them, then grab its location and append it to our array of active slice points, before calling a method called redrawActiveSlice()
. After that we will say:
if !isSwooshSoundActive {
playSwooshSound()
}
…of course we need to create the appropriate Boolean, set it to false and, in our Helper methods section, create the playSwooshSound
one (don’t worry, we’ll complete it soon enough).
Finally we need the touchesEnded
method, which will make the two active slices fade out in a quarter of a second. Now it is clear why we had to remove all actions from them before. If not, we would have them in the middle of a fade-out action and being created at the same time.
More helper methods
The redrawActiveSlice
needs to do several things: first, if we have fewer than two points in our active points array, we need to clear the shapes and exit the method (you will remember from the mathematics class that, to have a line, we need at least two points!); second, if we have more than 12 of them we need to remove the extra ones so that it looks that, while we are drawing, the first points are disappearing. This is done calling a nice new method I had never encountered on the array called .removeFirst(k: Int)
. Third we need to start the line at the position of the first swipe point, then go through each of the other drawing lines by each successive point. This is accomplished by creating a new UIBezierPath
, moving it to the first element of our active points array and then, looping over its elements from second to last, add a line from the last point where the line stopped to the next point in the array. Fourth and finale: set the path.cgPath
to be the .path
property of our active slices.
Finally, we implement our playSwooshSound
method which sets the isSwooshSoundActive
to true, generates a random number between 1 and 3, assigns the correct sound file to the soundName
local property, creates a swooshSound
action to play the sound file named soundName
(which should also wait for completion) and then run said action with a closure to asynchronously reset the Boolean to false.
This all works very nicely but I have already found a little bug: if instead of swiping on the screen I just keep my finger on the screen and move it slowly without ever lifting it, sounds get created very annoyingly without stop. Another thing is that if I touch with one of my finger-bones instead of with the fleshy part, the sound gets played but no line is created.
Enemy or bomb: AVAudioPlayer
This part of the app is about 80 lines of code in a single method plus a little extra thing, so it is quite hard.
The first thing we have to do is to create a new enumeration called ForceBomb
, which will determine when we need to force the creation of a bomb or not. This will make the game a bit kinder for players at the beginning. This enumeration will have three cases: never
, always
and random
, which will be called almost all the time.
Then, somewhere toward the end of our helper methods we create a new method: func createEnemy(forceBomb: ForceBomb = .random)
. Inside it, we create a new sprite node enemy property and a random number stored within the new enemyType
property. We then check if the force bomb parameter is equal to .never
, in which case we set the enemy type forcefully to 1
, else if it is set to .always
we will force the enemy type to be 0. All this will come clear much later but, by now, we can say that if we have the 0
we will create a bomb, while with the 1
we will create a penguin.
Now we have a huge if-else
statement. First we manage the if enemyType == 0
: we begin with created a new SKSpriteNode
container that will hold the fuse and the bomb image as children, setting its z position to be 1 (in front of the rest). We also call this container “bombContainer”. Second we create the bomb image, name it “bomb” and add it to the container. Third, we import the AVFoundation
framework, create a new property var bombSoundEffect: AVAudioPlayer?
and, if this property is not equal to nil
, we call stop()
on it and set it to nil
actively! Fourth, we create a new bomb fuse sound and then play it. This is done by extracting the audio file from our Bundle, exactly how we extracted files in the first projects (if let path = Bundle.main.url(forResource:withExtension:)
, then we optionally try to set a sound
property to the contents of the just extracted path of our AVAudioPlayer
and, if all this succeeds, we set this sound
property inside the bombSoundEffect
and we then call sound.play()
. Fifth and final: we create an emitter node with the file named “sliceFuse”, set its position to be (76, 64) so that it is at the top of the bomb and then add this node to the enemy
node.
At the end of this method we have the else
statement where we create a new sprite note from an image called “penguin”, run the action to play the sound file named “launch.caf” without waiting for its completion and set the name of the enemy to be “enemy”.
The last thing we need to do is to create code for the positioning of the enemy: first, we give the enemy a random position off the bottom edge of the screen, by using a CGPoint
with a random x
value and a fixed y
value just off the bottom of the screen and assigning this random position to the enemy’s position.
Second we need to create a random angular velocity, which is how fast something should spin. To increase the unpredictable feeling of this game we also create this as a random CGFloat
between -3
and 3
. The Documentation doesn’t specify which values should be good for the angular velocity, nor Paul explains them so we will just have to take them for granted (at least I tried, right?).
Third, we need to create a random X velocity (that will determine how far to move the body horizontally) that takes into account the enemy’s position. Instead of writing here some values you can see for yourself, I will just tell you that, if an enemy is created next to any of the edges, it will move in the opposite direction at a quite dramatic speed, which if it will be nearer to the center, it will move at a more moderate pace (all of this, completely random!).
Fourth, we need to create a random Y velocity, which Paul has determined as a random value between 24 and 32. A physics body’s velocity is measured in meters per second, and, looking below in code, we see Paul multiplying this value by 40!! I am sure there must be some other catch because if we even get 24 as a random value, that is 24 x 40 = 960 m/s, which means 3456 km/h!! That’s Mach 3! We should theoretically hear a sound wall breach!!! But then this is a CGVector
and … my math memory is not enough for this… there is a x and y component which should bring a result of a speed in meter per second … I have asked for help on the Slack, possibly someone will explain this to all of us because even Apple’s Documentation assumes one should know this.
Fifth and final: we should give all enemies a circular physics body where the collisionBitMask
is set to 0
so they don’t collide with each other. So, in short, we set the enemy’s physicsBody
to be an SKPhysicsBody
with a circle of radius 64 (that’s half the size of the image used for the sprite), its velocity
to this supposed vector which has been very kindly pre-calculated for us, its angularVelocity
to the random angular velocity we approached earlier and its collisionBitMask
to 0. We then add the enemy to the scene and to the active enemies array.
But we are not over yet: we need to implement the update
method because otherwise the sound would continue to play even if there would be no bombs on screen. This method gets called once every frame, which means just absurdly often! So, every time this gets called, we declare a variable called bombCount
and set it equal to 0
then, for every node in our active enemies array, if the node’s name is “bombContainer” we increase the bomb count by 1 and break out of the loop because there is no need to go any further. If the bomb count has instead stayed at 0
we stop the sound effect and set it back to nil
.
I would like to delve a bit into AVFoundation
but its 1.35am and I really need some sleep! Till tomorrow, folks!
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!