SpriteKit - no magic required
Hector Matos at Playgrounds Conference, 2017
What is a Sprite?
All right. So, a lot of y'all might have a question right now. You know, my last talk was on Type Erasure magic, right? So, I would like to consider myself a bit of a magician. If I were to read your minds, it would probably be something along the lines of; why is he dressed like that? Well, I'll tell you.
The thing was while I was trying to think of a costume, and by the way, if you don't know me, I wear costumes to every one of my talks. So, I was trying to think of a costume for this one, and I wanted to talk about SpriteKit. The first thing I thought was to look up the dictionary definition of ‘sprite’, and there it was: An elf or a fairy. So, you think I'd have a worse time trying to find a fairy costume but, you know, luckily enough my wife had one. Here we go. Either way, what we're really here talk about is SpriteKit, and what a sprite is, right?
If you look here, it says, "A computer graphic which may be moved on-screen and otherwise manipulated as a single entity." Dictionaries have always been very complicated. What I'm going to try to do in this talk is try not to make it as complicated. What do you actually need to know about SpriteKit?
Game development in SpriteKit
There's a vast wealth of knowledge out there, on SpriteKit. How to write in SpriteKit, how to do SpriteKit, how to make games in SpriteKit. Ray Wenderlich has some really great tutorials online, and the thing I was thinking about is that; Okay fine, there’s a lot of stuff about SpriteKit, what makes me unique, right? Other than the wings. That is that I think I can explain; game development and how that looks like in SpriteKit.
When we think of game development, we think really complicated things like collision detection, physics engines, complicated shaders, vertexes, and what even is a vertex, and writing in weird, troll-y languages like OpenGL that could really have only been written by one of these guys.
As I'm talking to you here on the stage, I want to let you know, I'm not going to pretend like I'm an expert at game programming, but I can create games because of SpriteKit. I can even create complex animations because after fiddling with it, I finally understood like three main concepts, and that's what I'm going to teach you all today.
The game loop, contact detection and collisions, and architecture.
Once I mastered these three things, it made everything else a lot easier because these three concepts are what are at the core of game development. Now, when you ask any game dev about any of these three, they're all flaming dudes saying, "I'm going to chuck my computer out of the nearest window because these are the things that take forever to do in a game." I'm going to try to make that simple for you.
The Game Loop
The first thing we're going to talk about is the game loop. I don't know about all of you, but I know that when I started hearing about games at the ripe old age of five, I would hear approximately two statements I didn't quite understand. I kind of had a high level knowledge of it but I didn't really understand what they were until I started diving into SpriteKit.
Let's look at the first one. "This game runs at a beautiful 60 frames per second." What does that even mean, right? In the history of animation, which is what a game is, it's just a series of interactive animations, there has never been a construct that has really explained to me what a game loop was other than- the Zoetrope. It's really hard to explain what that is, so I'd rather show you.
This is a Zoetrope (*points to screen*). As you can see, we have a bunch of Swift birds here, and we have a spinning table, some slats, a little white cylinder there. If you think of each bird here, each bird is a single frame. The spinning table is our game loop. When you think of every single one of those birds as one entity, you have a sprite. So now that we can understand what the general concept of a game loop is, let's look at what Apple tells us that a game loop is in SpriteKit. It's what this is (*points to screen*). There's a lot to see here, but I'm really just going to focus on the update function so let's zoom in to this graph here. That's Australian version of this slide (*Slide is upside down*). There we go that's better.
In our game loop, the first thing that we see is the update function. Now, what I want you to pay attention to is; look how small that is. The rest of the circle is really just SpriteKit doing everything else in a frame. Sixty times a second, this revolves around and around. You have your physics engine here. You have application of constraints, and then that really huge chunk of rendering time there. As game devs, this is one of the most difficult parts of game development, which is the update function. Because you have such little time to fit in all of this logic to update your game state, to do things like animations.
When we're focusing on this update function, the question you may ask is, "What happens when you go over time? When you go past what little amount of time you have, to do this update function?" That actually brings us to the next statement that I've heard over my years of gaming. "Oh my God, I hate this game. It's so laggy," right? The amount of time you have to process and update in each frame, like I said, is a source of a lot of pain, and when not handled properly, you get what's called “lag”. Now, basically, what lag is, if your code runs longer than that update function, the rest of that circle will not resize itself for you. The only thing that we can do to compensate for that extra amount of time is to drop the next coming frame to give that more space to complete.
When that happens enough times, you get something like this (*video clip of “laggy” game is shown on screen*). See that? Not smooth. I don't know if any of y'all have ever played Super Smash Brothers, but you really have to think on your feet when you're playing this game. When you have the game lagging like this, and you're hitting buttons at like maybe five buttons a second or something like that, it's pretty bad when you have to deal with this.
High performance code is very essential to game development. When you're in your update function, you really want to think about that. Let's take a simple example of what the update function looks like. In your update function, you might update state. Say we have Mario and you're pressing left, you're going to want to go in a direction, so we'll give it a negative value. So we'll update his position by one pixel times whatever the direction he's moving in. But, movement does not a game make, all right? Nobody just wants to see Super Mario run. Joke aside, games still have another important thing that they need, to be successful, right?
Contact detection and collisions
Mario doesn't just run, he hits coin boxes, he grabs feathers, right? So that brings us to contact detection and collisions, the second most difficult part of game development. Because not only does your game loop have your game state updates in there right. But it also has to do your physics engine, and your contact detection, at which point you have to figure out; hey, does this sprite, is it touching this sprite at this vector here, what is going on? What I love about SpriteKit is that it makes all of that so easy for you. It encapsulates that in such a great little construct in Swift. Can you guess what it is? Try it, protocols. Thank you.
That protocol is called the SKPhysicsContactDelegate. You have two functions, didBegin(_ contact: ), didEnd(_ contact: ). It passes in an object called the SKPhysicsContact. So if we look at that object, we see just about everything you need for a contact. You have bodyA touching bodyB at this CGPoint. But when you're looking at those bodyA and bodyB, you see SKPhysicsBody. What is that? What does that look like? Well, it looks like this. You have the categoryBitMask, a collisionBitMask, a contactTestBitMask.
If anybody has done SpriteKit development, you'll know there's a lot more than that, but I really want to focus on these three things because these three properties is what unlocked everything for me when it came down to understanding what a contact and a collision was in SpriteKit. Because when you think about what these three things are and you get those set properly, then you get collisions and contact detection for free in SpriteKit. Let's look at what's needed for both.
At a minimum, for collision behavior and contact detection, each sprite must have a physics body. It cannot be nil, they both have to have a physics body. Here's something like what that looks like. You have your sprite notes, and we're setting physics bodies on them. Pretty easy. Now, both physics bodies, not the sprites but the physics bodies, must have a categoryBitMask. Now what's a categoryBitMask? CategoryBitMasks are a way for sprites to have unique identifiers. There's a way for the sprites to tell the game engine, this is what I am. It looks something like this. The categoryBitMasks and the other BitMask’, they accept an unsigned integer, a UInt32.
How do we make them unique, though? If they're supposed to be unique identifiers, how do you make them unique? Well, if anybody's done anything with computer science, BitMasks must be a power of two to be unique when combined together. So just remember that. They must be a power of two because I'm going to say this quite a few times. That was the thing that I had trouble thinking of. When I did my first SpriteKit game, things were colliding with things that they were not supposed to be colliding with and it's because none of them were powers of two, so just remember that.
So now for collision behavior, now that we have that, both of them have a category set type or BitMask. At least one physics body must have a collisionBitMask. That's the physics body that wants to collide with something else. Kind of looks something like this. Our Mario was 1, our coin box is 2, and the marioPhysicsBody wants to collide with 2, which is our coin box. We just defined that. Now once you have that, collisions are automatic in SpriteKit. You don't have to do anything else. They will bounce off of each other. So you get something like this for free (*points to screen showing animation of Mario colliding with coin box*). So SpriteKit, I just want to say; I love you this much.
Now, moving on, for contact detection, at least one physics body, since they both have categoryBitMasks, for contact detection, it's very similar. It's the contactTestBitMask, and that looks just about the same. But now, what's this? We have something a little more complicated. Because Mario wants to hit a coin box, wants to know when he hits the coin box, and he also wants to hit a feather. So that way, he can start flying. This may look a little confusing, but the thing you need to remember is that BitMasks are powers of two, and when you combine them, you use what's called the logical OR operator. SpriteKit takes care of the rest for you.
At the binary level, what we're looking at is something like this. We have our 4, which only has one bit turned on, this is the key to what makes it unique. Then we have our 2, and when you combine them together, the one gets squished down to that value right there on the bottom, and it's able to use that as a Mask, whenever it decides to figure out; hey, which sprite is actually hitting which sprite? Because it's only using 32 bits to do this calculation.
That was a lot of information but the thing you gotta remember is BitMask are the most efficient way of comparing sprites. All BitMasks must be a power of two, and when you want to combine categories, you do that with a logical OR operator. So here's an easy way of thinking about it. Say I am Mario, and I hit a coin box, and out of the coin box, pops a feather. So the hat is my categoryBitMask, right? The feather is the contactTestBitMask, and that's what gives me my cape.
So one last thing, if we want to get any of this working, is that your GameScene, which is what your sprites exist in, it has a physicsWorld property, and it must have a delegate set. It looks something like this. So here, in .sceneDidLoad(), we're accessing our physicsWorld and its contactDelegate, and we're just setting it to self. That brings us back to our wonderful protocol, do y'all remember that wonderful protocol? The SKPhysicsContactDelegate? Once all of that is set, and once you have everything set up, this function starts to get called, and you're good to go.
However, there's a couple of things that I've said so far, which is, most of your logic's going to go in your update function, and most of your logic is going to go in this function too. But if you ask any game dev, I actually talked to a game dev the other day, and he told me, he was like, "You know, I wanted to tell people it's okay to have a thousand if statements in an update function." But I would beg to differ, because that brings me to my next topic, which is important, and not just to game devs, but to everybody else, and that is; architecture of your code.
I think that with the proper architecture, you can reduce the amount of logic that you have in your update function and in your didBegin contact function. Now that we're at architecture and we're pretty much at the final bit of this talk, right ... a little thirsty. #productplacement.
So, let's move on. First thing is, y'all remember the BitMasks, right? They had magic numbers. As we all know, magic numbers are bad juju. Worst part of programming for me is when I see a magic number or stringly typed API or what have you. When I'm doing game dev in SpriteKit, I personally hate keeping track of this. When you have a big enough game, you're going to start losing track of which number belongs to which, right? Personally I'd rather have code that looks like this (*points to screen*). This is compilable code.
Before I actually show you this compilable code, let's go back to our BitMasks. In Swift, if you didn't know, BitMasks are represented by OptionSetTypes, another protocol. Let's take a look. What we have here is what I would like to say is good architecture. This is the pseudo of Super Mario World game, it doesn't actually build, I haven't put any more effort other than to show you how you can architect the game.
So, first off, what I want to do is I created a SpriteType struct that conforms to OptionSet, and this is all the code that you need. It starts off with the none type, which is a zero, and now we have our powers of two. This weird operator, you may remember from the NS_ENUM days. I know when I looked at it, I didn't understand what was going on. Basically it's a bitshift. It starts with 1, which is; zero, zero, zero, zero, zero, zero, zero, one, and it moves it over by one. Then the next one, it moves it over by two. So there's still only one bitset, so we still have that unique power of two.
So Mario, coin box, feather, they each have unique values and now that they're represented by an OptionSetType. I also have everything that OptionSetType gives me, which is everything that you can do with BitMasks. Combining them, comparing them, seeing if a BitMask contains another BitMask. That's one part of the puzzle.
The next part is another protocol called the GameNode. GameNode is going to be something that as I create every single sprite, it's going to conform to this protocol. It gives me this. I'm creating a categorySetType, a contactTestSetType, a collisionSetType, and they are the same type as the same SpriteType that I just created. If you look at this extension of the protocol, all it's doing is it's forwarding it to the underlying physicsBody, so this is an extension of this protocol, where whoever conforms to it is a SKNode or a SpriteNode.
I'm doing all of that for you. So all you have to do, whenever you're creating a sprite, is make sure you're calling your setUpBitMasks in an init function or something, somewhere, and it'll look exactly like this. No numbers, no magic numbers. You have a nice array API here for OptionSets.
Now what about this collidedWith function? How do I get it to work with this? If we look at my GameScene, I have my regular set up functions where I'm setting the physicsWorld.contactDelegate like I told you. Here's our update function. Our game loop. In the game loop, this would normally be hundreds of lines of code long in a really big game, but what I'm doing is I'm forwarding this call to the children because the GameScene can get really lengthy. I kind of believe in the single responsibility principle, and I kind of believe that each node, each child, should handle the logic themselves. Really, all this is is just a recursive function down here that calls updateChildren on all of the children.
Let's move on to the contact. This is the second function and all of this belongs in the same GameScene, so you can imagine a really huge class. So I'm doing something very simple here. I'm getting the node out of the contact, and I'm casting them to GameNode. If they aren't GameNode, then I don't care. Basically if nodeA contains, the contactTest for nodeA contains the category of nodeB, then I just call collidedWith, and I do the same thing in reverse, in case they both want to be notified of contacts to each other.
Basically, that's it. With a few lines of code, a protocol, some indirection here and there, you have what I think is pretty good architecture and a good starting point for a SpriteKit game.
Thanks and credits
With that, if you're interested in using the same kind of architecture for your games, I have open-sourced it here at: github.com/krakenDev/SuperMarioWorld.
This is my website, I run the blog at Krakendev.io.
My Twitter handle is @allonsykraken. So, in a non-creepy way, I urge you to follow me. With that, thank you very much.
If you enjoyed this talk, you can find more info on the conference here: