Introduction
Today I am not in a hurry … I am the hurrying! I have so little time it is causing my head to turn so forgive me if I will skip the report from yesterday’s feeling and try to dive straight into the challenges…
Review for Project 26
Here is what we learnt in the Marble Maze project:
- We can run a completion closure when an
SKAction
finishes. This lets us perform some other work when an action completes. It is worth mentioning here also the other option because the explanation is really important: “Once we create an instance ofCMMotionManager
we can start reading accelerometer data straight away. FALSE: accelerometer data is only available after we have calledstartAccelerometerUpdates()
on our motion manager. - A physics body’s
collisionBitMask
determines which other objects it bounces off. By default this is set to “everything”. I try to understand this by reading the property name without the “BitMask” part so, “collision” = … well, you say it, what it will collide with and react. The other part of the question was asking whethercategoryBitMask
would have been the one, which instead determines what type of thing this physics body is. I will need much more time to get comfortable with this as I feel my synapses and neural cells digging some fresh space in my brainfor this! - The
linearDamping
property of a physics body lets us control how much friction applies to it. This should be a number between 0 and 1. - Calling
fatalError()
will always crash our code. This is useful to cater for cases that must never happen. I want to mention again also the other option because, in the video lesson, we were not given a proper explanation about the bitwise OR operator|
: the question was “The|
and+
operators do the same thing” and the explanation was “they are quite different operators:|
combines numbers, whereas+
adds them. For example,4 | 3
is7
, but4 | 4
is4
, because you can only add each number to the mix once”. I think this will be important for us in the coming challenges. - Enum raw values are underlying values that sit behind each case. These can be strings, integers, or something else. We have never met enum raw values other than integers but I’m sure we will, sooner or later. The advantage of integers is that the Swift compiler automatically provides other cases if we write just one of them!
- Looping over a string gives us each letter one at a time. Be careful, though: strings don’t let us read individual letters using an integer position.
- The
components(separatedBy:)
method of strings sends back an array of strings. Some strings might be empty depending on what our source data was. - Physics bodies with
isDynamic
set to false won’t get moved by gravity. Non-dynamic physics bodies are actually faster for SpriteKit to simulate. - Pi radians is equal to 180 degrees. UIKit and SpriteKit both work in radians rather than degrees. (unfortunately…)
- All iOS devices have accelerometers. Even the very first iPhone had an accelerometer built in.
#if targetEnvironment(simulator)
is a compiler directive that lets us compile code only for the simulator. Any code inside this directive won’t be compiled for real devices – it’s as if it doesn’t exist.- Bitmasks can be combined using
|
. This operator is called “bitwise or” and it combines two numbers together.
That’s it! Let’s move on to the challenges!
Challenges for project 26
Challenge 1: rewrite the loadLevel()
method so that it’s made up of multiple smaller methods. This will make your code easier to read and easier to maintain, or at least it should if you do a good job!
I tried a few different approaches in order to compact the code as much as possible but there were a few things not collaborating too much or that would have not saved me enough lines of code (write a 5-line method to save 3 lines in total is not really a nice idea of refactoring…).
Finally I tried the simplest approach to see if that would have been enough: I created four different methods called, for example loadWall(at position: CGPoint)
, and moved the creation code inside.
I then saw that circular nodes have many lines in common but some of them are separated by others so I gave up on that side for now.
Coming back at it I created another method called createNode(called nodeName: String, at position: CGPoint) -> SKSpriteNode
. Inside I put the node’s initialisation, the node’s naming and the node’s positioning before returning the node. The method is 7 lines long including braces and saves about 12 so we are at a +5 win. Maybe it’s worth it, after all!
Challenge 2: when the player finally makes it to the finish marker, nothing happens. What should happen? Well, that’s down to you now. You could easily design several new levels and have them progress through.
The basic logic here is that we need to be able to support multiple levels in our code based on multiple text files so I created a new property called currentLevel
and set it equal to 1
. Then, in loadLevel()
I modified both guard
statements to have \(currentLevel)
instead of 1
in the string. Finally, in the playerCollided(with:)
method, in the else if node.name == "finish"
block, I called this:
player.removeFromParent()
currentLevel += 1
loadLevel()
createPlayer()
For testing purposes I made it so that the player is always created at the same starting point. By now I am having tremendous difficulties to reach the end of level 1 as it is and when I load level 2 it loads something that doesn’t correspond at all with my text file, which sincerely I do not understand.
For future testing purposes I added an acceleration
Double property set to 25.0
(half of the previous value) and modified the corresponding entry in the update
method. This should allow for an easier gameplay at the beginning.
One of the many, too many things I have to understand is that when you want to load a new level of nodes… the other nodes must go away otherwise they will just overlap and produce crazy results. But to do so we need to have something to remove. You see, when we created the first level we never thought we would have had another one so we need to go back a few steps.
I also profited from making a separate method of the createBackground
code, so that any line saved is a line gained (and I did the same for the createScoreLabel
one.
For this we need an array to store all component nodes we added to the scene in the level loading process so that they will be easily removable when the level ends. It will be an empty array of SKSpriteNode
s by now. Next we need to call levelNodes.append(node)
at the end of each of the refactoring methods we had introduced in challenge 1.
Next, when we hit the “finish” node, we need to remove each node from the parent and there is a functional method called .forEach
that allows to call a closure on every element of the array. So, calling levelNodes.forEach { $0.removeFromParent() }
would just do the trick. Thanks to Rob Baldwin (once more) for suggesting this.
To avoid needless crashes we also create a maxLevel
integer variable so that if we hit the finish cup when the current level is equal to the max level, we just go back to level 1. We could also just present a “Game Over” message so that the game ends. It’s a choice.
… well … what can I say … I now tried to build and run the app again and everything is absolutely messed up… the loaded level is not level 1 nor any other … I have no idea what is going on…
I thought it could be an issue with the createNode
method but removing it and bringing back the previous code didn’t help. The level loaded is a strange mix which is quite not right…
… ok … something seems not to be right at all with the level-nodes array and the appending part or something like that… Otherwise it would not be possible that everything was fine before and now it is not. … insert few minutes here… found the mistake… too embarrassed to say what it was… let me just say that an if-else
statement has not break
command word inside it…
Now I will try to get to the end of the level and see if the loading works. Still, every time I build this game I get a purple warning from Core Motion. I wish we were taught how to handle these warnings … but I guess I am getting nervous only because I have so little time today. Learning how to code under times constraints is very unproductive…
… fine, it all works. Phew… Let’s move on to the last one.
Challenge 3: add a new block type, such as a teleport that moves the player from one teleport point to the other. Add a new letter type in loadLevel()
, add another collision type to our enum, then see what you can do.
Let’s proceed in order: let’s add a teleport
case to the CollisionTypes
enum equal to 32
, double of the last one, so that they can add together without overlapping.
Inside the loadLevel
method I added two extra else if
so that the letter “d” would mean departure and “a” would mean arrival, calling respectively the method loadTeleport(at:isDeparture:)
with true
in the first case and false
in the second case.
Down below I wrote the loadTeleport
method which is very similar to all the other ones with just a check for it being a departure point or an arrival one and changing the name of the node accordingly. I also added a “bubbly” feeling so that the teleport shrinks down a bit and expands back every half a second and repeats this indefinitely.
At the bottom of the playerCollided(with:)
method we create two extra if else
cases (I know, a switch would have been more appropriate but I will possibly get there in another run) for “departure” and “arrival”. To avoid crazy stuff we should consider adding a Boolean that checks for the player being in a teleporting state or not otherwise once the player arrives to the new teleport it would just be bounced back. That could be solved with a var isTeleporting = false
at the top of the class.
By now I assume there would be only two teleports in the game, no more. So, in the else if
block I checked that the teleporting Boolean would be false then, if there would be a node called “arrival” anywhere in the scene, I would set the Boolean to true
, temporarily deactivate the player’s physics body’s dynamic to avoid any interaction for a few instants during teleportation, then performe a bunch of actions (move to the center of the teleport, scale down, teleport, scale up, restore dynamic (with a closure)) in a sequence, then switch the names of then nodes so that travel back would be possible without writing another method (which may still be the best way in the long run but …) and then setting the teleporting Boolean back to false
after 2 seconds.
I tried it out and everything works. Of course, if you get to the arrival teleport first, it will not teleport you, which from one point of view is undesired, from another it could be a role-play element that will ask the player to look for the good portal. I could improve this with changing the colours of the portals, something like green and red but, apart from really not having time today, I think the scope of this challenge is accomplished.
I admit it, after almost 3 hours of trying and bashing my head I checked Rob’s proposed solution for inspiration and then implemented my own version in the end. I feel that if I had spent about 3 to 4 more hours on this I could have gotten there, which is already great if I think of it but … as today I really do not have that time, I could not. Also, SpriteKit games are not really my forte and as much as I like games, I wish I could learn how to create more advances apps instead of games. So, all the while learning a lot with them, I do not want to kill myself with them.
So, that’s it for today, I need to get back to work!
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!