Hacking with Swift – Learning Project 26
After the great and most productive day that was yesterday I will have a very hard time replicating it today as I spent all the morning at the passport office for renewal … I arrived at 9am, exactly when the office opens and … I have 43 people in front of me! Luckily enough, a young woman gave up before me and handed me her ticket just because… nice how things can turn positive with just a very simple act. I obviously returned the act after my turn passed, spreading some positive energy in the room!
Today is a game’s day and we will learn a lot of new exciting stuffs, among which stand out Core Motion and the loading of a game level from a file! Without any further hesitation, let’s get started, shall we?
Setting up
Create a new iOS Game app with SceneKit and call it Marble Maze! In the general settings change the Deployment info to make our app for iPad only and disable every orientation except for Landscape right. We will be tilting the device so we do not want the UI to flip on its own.
Perform the usual cleanup for every SceneKit project and download the assets for this project. Put the images in the assets catalog and the level1.txt file into the project navigator.
Loading a level: categoryBitMask
, collisionBitMask
, contactTestBitMask
This loadLevel
method is really long even though there is nothing we have not seen before so it is very helpful to go through things once more to help them sink. First, we check that we have a file called “level1.txt” in our bundle otherwise we call the fatalError()
method (which we meet here for the first time) to say that we could not find it; second, we optionally try to extract a String from the contents of that file (and we again call fatalError()
if we cannot do that, for any reason); third, we store the components of that strings (separated by \n
) in a constant and fourth, we start to dance!
No, seriously, we create a nested loop which, in the outer loop, goes over the (row, line)
in the reversed and enumerated string array constant we just created and, in the inner one, goes over the (column, letter)
tuple of the enumerated array. The reason the reversed is that SpriteKit
considers the Y:0 coordinate to be on the bottom of the screen (quite naturally), while UIKit
considers it to be at the top of the screen.
Once we are inside the loop we determine a position constant which is a CGPoint
with x: (64 * column) + 32
and y: (64 * row) + 32
. 64
because this is the width of the sprite and 32
because SpriteKit
determines the position based on the center of the sprite (which is, quite obviously, size / 2
). Then we write an if-else
control flow statement which goes over the letters in the text files and interpret them (absolutely brilliant!). If an unknown letter is found we call fatalError()
while if we find a space we just do nothing at all.
We then create a new enumeration atop our class called CollisionTypes
of type UInt32
(32-bit unsigned integer) with 5 cases inside: player, wall, star, vortex and finish. Each one of them will have a raw value of, respectively, 1, 2, 4, 8, 16, so that any one of them cannot be made by adding together any two others. This part of the mental model didn’t quite sink with me but I can see at least part of the reasoning!
Next, we go on filling up the code in the if-else
block, each time creating a new node from an image, setting the position to the position
we determined earlier, then setting the physics body to a rectangle for the wall block, to a circle for everything else, then the physics body’s category bit mask to the appropriate raw value in the enumeration, the .isDynamic
property of all of them to be false
(we do not want them to react to the laws of physics such as gravity!). Now for the specific code: the vortex will be the thing the player has to avoid so it will have a contact test bit mask equal to the player’s raw value, meaning that it will react (and notify us) when the player enters in contact with it and also a collision bit mask of 0
, which means “nothing”. In short, by default every object bounces against every other one while setting this to zero will set this to “bounce against no one”. Of course we will then write code so that if the player enters in that area it gets destroyed.
Before finishing, let’s also add the background image at the center of the screen, with a replace bend mode and a -1
z-Position!
Tilt to move: CMMotionManager
Before starting to play with the accelerometer we need to create a player, which is an implicitly unwrapped sprite note. Then, we write the createPlayer
method, which initialises the sprite from the “player” image, positions it at a precise point for level 1 (wait, there is no other level, but this is a hint of things to come!), set its .zPosition
to 1 so that it draws in front of everything else, create its physics body from a circle, set the allowsRotation
to false and the linear damping to 0.5
. We don’t want the body of the ball to rotate because the reflections on the marble should not rotate (not really clear why but I will let this slip for now). The linear damping value will apply a lot of friction so that the game is still hard to play but, well, possible!
The player’s category bit mask will be the appropriate raw value from the enum, its collision bit mask will only be the wall raw value while the contact test bit mask will be a bitwise sum of star, vortex and finish raw values. This is accomplished by placing a pipe |
between these values. In theory this is called the bitwise OR but, from how we are using it, it has nothing to do with the ||
that we used to know. In any case, this means that we want to be notified when our player collides with either the star, the vortex or the finish node so that we can do something with it.
Once this is done we want to call the createPlayer
method at the end of the didMove(to:)
method and then set the physics world’s gravity to .zero
. This will give the game a space-like feeling which is needed if we want to use tilting to control the game.
Now, we need to set a hack for testing on the simulator and I will not go over it here, as it is pretty simple: in the code, just look for the lastTouchPosition
property and then for the touchesBegan
, touchesMoved
and touchesEnded
methods. Then, inside the update
method that we need to create we will write a line of gravity creation code, which will create a gravitational pull proportional to the distance of the player from the user’s tap on the screen. Pretty powerful, right?! This will be then wrapped inside an #if targetEnvironment(simulator)
– #else
block so that, in case we are on a real device, the simulator code will not be run.
Finally for the funny part which … is just a few lines of code: first import the Core Motion framework, then create an optional CMMotionManager
variable property then, in didMove(to:)
, instantiate it and start the accelerometer updates (« Scotty! I need more power! »). Then, in the #else
block we will check if we have some accelerometer data and therefore set the gravity of the physics world to a vector whose dx
and dy
value are proportional to the data we get from the accelerometer. End all this with #endif
.
Contacting but not colliding
Let’s create a score label and a score property with a didSet
property observer that will update our score label as the score gets updated.
Conform to the SKPhysicsContactDelegate
protocol so that we can get notified when a collision occurs. Inside didMove(to:)
add the physicsWorld.contactDelegate = self
just below the space gravity line.
Now implement the didBegin
contact method. Inside we need to check that there is a first node (called nodeA
) and that there is a second node (called nodeB
) both of which are part of a contact event (which SpriteKit describes using the SKPhysicsContact
class). Then, if the first node is the player we call the playerCollided(with: nodeB)
method and viceversa if the player is the second node, exactly as we did in project 11.
Now we need to write the real game logic, that is how the game reacts according to which item the players bumps into. Create a Boolean for when the game is over and set it to false
. Then, at the top of the update
method, check that the game is not over otherwise return from the method.
Finally we need to write the playerCollided(with:)
method: if the node the player collided with is a vortex we need to set the dynamic of the player’s physics body to false, the game over property to true and decrease the score by 1
. We then declare three actions: a movement to the vortex’s position so that it looks like it is sucking the player in, a scaling down during a quarter of a second and a remove from parent action. We group all of them in a sequence and then run that asynchronously so that, at its completion, we create a new player and set the game over too false.
In case the node is a star, we remove it from the game and increase the score by 1
. An extra else
case is left for the challenge so that we can program the next level.
I think I will face the challenges tomorrow as I have still quite a bit of work to do on the music notation side, but I will see later tonight. Now I need some pasta all’arrabbiata!
You can find the finished code for this project here.
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 about 20 great books on Swift, all of which you can check about here.
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 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!