On March 13 2018, Apple announced WWDC 2018. As in previous years, tickets were available via the ticket lottery, as well as Apple’s scholarship program, where 350 students apply to receive a free ticket and lodging for WWDC.
Although I had just started learning Swift, I decided to try my luck and apply for a scholarship anyways. I was incredibly ecstatic to be awarded a scholarship, and in June 2018 I attended WWDC and learned a ton as well as met tons of other scholars and Apple engineers.
In this blog post, I will walk through how I created DoublePong, my WWDC18 scholarship application Swift Playground. DoublePong is an adaptation of the classic Pong game, with paddles on all four sides of the screen.
On the original Mac version, DoublePong is played by moving your mouse horizontally or vertically to move the paddles. On the adapted iOS version, there is support for tilting your device or using your finger to control the paddles, but this tutorial will only cover the original macOS version.
⚠️ Note: I have not modified most of the code used in this blog post since my original WWDC scholarship submission except to update it to work on the latest versions of Swift and Xcode. Some of this code does not exhibit best practices, and shouldn’t be reused, but it has been left to demonstrate how I created my original winning scholarship submission. To assist you, I have placed notices in some places where I feel this is prevalent.
Get started by creating an empty macOS Playground in Xcode (File -> New -> Playground). Then, press Command-1 or select View -> Navigators -> Show Project Navigator to show Xcode’s project navigator sidebar on the left with your Playground’s structure.
To simplify some of the code in this post, I’ve created a small Swift file with some useful extensions, such as a convenient way to add multiple subviews. Click below to download this file, which is required for some of the code we’ll write later.
After downloading and extracting the archive, put
Extensions.swift into your Playground’s
Setting up the scene
Under the hood, DoublePong uses SpriteKit, Apple’s 2D game engine. To use it, we will create a custom
SKScene and present it in our playground’s ‘live view’. Enable the playground’s live view now by pressing Option-Command-Enter or clicking the overlapping circles on the top right of Xcode’s toolbar to show the Assistant Editor.
Now that our playground is ready, navigate to the main playground file by clicking it in the project navigator, then add this code, which will set up our scene and show it in the live view in the Assistant Editor:
The above code sets the scale mode of our scene, then embeds it within a
SKView which is embedded in an
NSView, which we then display in the live view. You’ll notice that this code won’t compile yet, as we haven’t created the scene that we are using.
To allow us to use the scene everywhere, create a new file named
Global.swift in the
Sources folder, where we’ll place all our global variables.
⚠️ Note: As per the note at the top of this post, some of the code shown here was written by me a long time ago, and isn’t the best. In particular, you should avoid creating global variables as this can clutter your code. They have been used here because of the relatively small size of a playground and to simplify the code.
In the new file you created, make sure to import the
SpriteKit frameworks, then create a private constant for our scene:
We’re getting closer, but our playground will still fail to run, because Xcode doesn’t know what class
To fix the issue, create a new file named
Sources, where we’ll place all our custom scene code, where we’ll override
didMove(to:) function to set up our scene:
As explained in the comments, the above code sets up our physics and ‘contact delegate’, which will let us receive notifications whenever two of our sprites, such as the ball and paddles we’ll add later, collide (touch).
Your playground will now run - but it’s just an empty scene! In the next section, we’ll add our sprites and allow the user to move their mouse to control the paddles.
Adding the sprites
Now that our playground runs, we need to add our ball and paddles, or we’ll be stuck with an empty screen!
Go back to
Global.swift and add the following variables, which will hold our balls and each of our paddles. The additional constant,
themeColor, can be any color you’d like and will be the color of all the sprites.
We also need to add some bit mask variables, so that SpriteKit knows which sprite is which and can tell us when they collide. Later on, we’ll set up the paddles and balls with their bit mask and tell SpriteKit to notify us when the sprite collides with another bit mask.
Note that these are
let constants as we don’t want them to change, while our sprites are variables as we’ll set them up separately in
Scene.swift. Go to it now and we’ll add a new function in which we’ll set up our ball sprite:
In the function, we’re setting the name of the sprite so we can easily identify it later when detecting collisions. We’re also positioning the ball at a random position within our game grid using the new
CGFloat.random(in:) function introduced in Swift 4.2.
We then set the ball’s
SKPhysicsBody, which determines how the ball bounces and acts in the scene, specifically:
frictionis set to 0 to avoid it slowing down when it bounces, making the game fun
restitutionis how much energy lost when the ball bounces off another sprite
linearDampingis set to 0 so that no damping, which simulates air friction, is applied
We have now set up our ball, so if you add
setupBall() to our
didMove(to:) function from earlier and run the playground…… nothing happens!
This is because while we have now created and set up our ball we still haven’t added it to our actual scene. We’ll set up our paddles and then return to this later, so we can add all our sprites together.
To set up the paddles, we’ll add a new function named
setupPaddles() where we’ll set up all of our paddles:
As you can see, we are first defining our sizes and some random positions for our paddles. Then, we use the helper method that was included in
Extensions.swift (which you should have downloaded earlier in this tutorial) to quickly create each paddle with the size and theme color we have set earlier.
cBM is the
contactBitMask, and it tells SpriteKit which of the bit masks (that we created earlier) identifies this sprite. It goes hand in hand with the
contactTestBitMask, which tells SpriteKit which sprites it should send a collision notification for. For example, if our
cTBM for the right paddle was
leftPaddle, it would send a notification (which we haven’t set up yet) when it collides with the left paddle. Here, we are setting all of the test bit maps to
Ball, because they will all collide with the ball.
Now that we have set up all of the required sprites, go ahead and add the following line to the bottom of
didMove(to:), which will add all the sprites to the scene.
If you now run the playground, you’ll see the sprites are all created and the ball will start bouncing around!
Before we set up our collision detection, we need to register for mouse events, which will allow us to move the paddles based on where the user moves their mouse.
We’ll create a new function named
registerForMouseEvents(on:), which we can call to register for mouse events on our view:
We are setting up our tracking options, specifically, to only get events in the visible rect, and get mouse entered, exited, and moved events. We are then using
NSTrackingArea to add the tracking area to our view based on it’s frame size.
Call the new function at the top of
We will now create a new function where we handle the mouse events and move the paddles accordingly. This involves quite a bit of maths and calculations to make the paddles “snap” on the edges and avoid the paddles from exiting our scene!
mouseMoved(with:) function, where we’ll first set up some variables to make it easier to calculate the new positions of the paddles.
I have added comments to each variable to make it clear what they all do, so you don’t get confused when calculating the paddle locations.
Now, we’ll add the actual calculations below the new variables, starting with the top and bottom paddles:
We are using an if statement to calculate the position based on the variables we defined earlier. If the mouse location is within the scene size (with the padding included), we’ll simply use the location for the paddles. Note how we are adding the paddle length to make sure the entire paddle is within the area. Otherwise, if the mouse location is outside the padding, we’ll use the “maximum location”, or the “minimum location” if it’s outside the padding on the left side.
Using this padding system means that when the user moves their mouse to the very edge of the screen, it will snap the paddles to the max/min location, making it feel nice and clicky.
We’ll do the same for the left and right paddles, following the same patterns:
If you now run your playground, you will notice that moving your mouse now moves the paddles as you expected!
Next, we will detect collisions to increase the score when the ball hits the paddles without hitting the edges of the screen.
Add a new variable to
Global.swift where we can track the score:
We’ll then add a new function which detects collisions and acts accordingly.
First, we will simply set our first and second body’s names to a variable so we can use them more easily. We’ll then make sure that one of the bodies is a ball and that is has a physics body, otherwise we can’t continue to use the collision.
Then, if the first body is one of the paddles, we’ll increase the score and velocity of the ball accordingly:
The new score is based on the current velocity of the ball (so you get more points as the score increases). We’ll also make sure the ball is going fast enough and fix it’s velocity or increase it if it is already fast enough. As you can see, we’re using ranges, such as
0...100 to make the velocity normal if the ball is going very slowly. It is required to keep the negative velocity to avoid the ball changing direction.
Your playground will now run successfully, and the ball will start speeding up as you play! However, although the score is already updating, you can’t see it because we aren’t displaying it anywhere.
We will now add a score label to easily see our score while playing. Add a score label to our list in
We’ll also add a
didSet item to our original score variable, which will automatically update the score label whenever the score changes. This lets us avoid redundant code.
We now need to set up the new label and add it to our game view, so we’ll add a new function to
Scene.swift which creates our label. We’re doing this in a separate function so we can add more labels later on.
As you can see, we’re using our convenient method from
Extensions.swift to create the label and set it’s properties, such as position, text alignment, text size, and color.
⚠️ Note: As per the note at the top of this post, some of the code shown here was written by me a long time ago, and isn’t the best. In particular, you should really avoid hard coding the position of elements on the screen. It works for this playground because of the fixed size of the playground.
Now, we just need to call our new function and add the new label we created as a subview. Add this code to
The playground now runs successfully, and the game now works and displays your score, updating it whenever the ball collides with one of the paddles!
We’re almost done building a completely functional copy of DoublePong! The last thing we need to add is lives, so that the game is over if the ball touches the edges of the screen too many times.
Add a new
livesLabel and it’s corresponding
lives variable to
Whenever the lives are updated, we use a special method built in to
String to show a heart emoji for each of the remaining lives.
As before, add a new line to
setupLabels() where we’ll set up our new lives label:
The setup is the same as we did before for the score label, using the convenient method from
Next, we’ll add a new check to our collision detection to remove lives when the ball collides with the edge:
If the user has more than 1 life left, we remove a life. However, if all the lives have been used up, we should end the game. However, this blog post won’t cover that.
You now have a working version of DoublePong, my WWDC18 scholarship submission! I chose to remove some features to make this blog post shorter, but I’ve added them to the example playground, available below. If you’d like, you can try to implement some features yourself such as a game over screen/restart button, then check out my code to see how I chose to implement it (which may be different from your implementation).
Although DoublePong is a relatively good SpriteKit game, I also made this blog post in an attempt to encourage and help out students considering applying for a WWDC 2019 scholarship! I had an incredible experience last year and I strongly recommend anyone considering an application to go for it!
I hope you enjoyed this in depth tutorial into how I built DoublePong! Make sure to subscribe below to receive future blog posts in your inbox!
Have any questions or comments? Applying for a WWDC scholarship? I’d love to answer any questions you have or help you out! Just email [email protected] and I’ll try my best.
Thanks for reading 🙌