How to Make Pong in SpriteKit: Part I
Setting up the basic structure of a game
by
Lou Franco
In the previous article we learned that SpriteKit games are based on physics simulations. We put an SKScene
on a UIViewController
and then let a ball fall to the ground based on gravity.
This game isn't very fun.
But, it's helpful to understand SpriteKit games this way. Let's think about Pong.
Pong is in a frictionless, drag-free world
Pong is meant to be like Ping Pong. The players hit a ball back and forth until one of them misses. We are looking at it from the top down.
By default, SpriteKit assumes the game environment has Earth gravity and air. The air causes drag that slows down objects. There is also friction between the objects and energy loss due to collisions. The defaults are all set up to mimic the real world.
But, Pong is not the real world. In Pong, we have perfect bounces and a frictionless, drag-free, gravity-free environment.
In that environment, we have walls on the left and right and paddles on the top and bottom. The walls are completely static and just exist to bounce the ball. The paddles are controlled by the users and also bounce the ball.
All bounces are ideal and do not slow down the ball.
If the ball gets to the very bottom or top of the screen, a point is awarded and the ball is moved to the center to start again.
Let's build it step by step.
Start with an empty scene
From the last article, we learned that the minimal SpriteKit Scene is
import SpriteKit
import GameplayKit
class PongScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
startGame()
}
func startGame() {
// We will call this after the game is over to start again, so
// start by removing all of the nodes so we have a blank scene
self.removeAllChildren()
}
}
I added in a conformance to the SKPhysicsContactDelegate
protocol because we'll need that later for collision detection.
The game loop and manual updates
Another useful function to override is update(currentTime:)
which is called on every iteration of the game loop. The game loop is how this scene becomes dynamic and interactive.
We will set up all of the objects, their physics, and put in delegates to detect collisions. Then, SpriteKit will start a loop.
On each iteration, it will:
- Check for finger touches on the screen and call any of the various
touches
functions to let you know. - Call the
update(currentTime:)
function -- THIS IS IMPORTANT -- this gives you a hook to manually change anything you want apart from the way the physics world is affecting them. - Loop through the bodies and see if they have any velocity or acceleration, whether gravity is affecting them, etc.
- Apply those forces and update their positions.
- See if any objects touch each other and how that changes the forces on them and update their velocities, etc for the next iteration.
- Tell you via the delegates if any two objects have contacted each other.
- Render the scene by drawing the nodes
So, to prepare, we'll make a stub for update
that we can use later. It tells you the time because each iteration of the loop can take a different amount of time, and if you are trying to do a smooth animation, you'll have to take this into account.
// This function is called on every iteration of the game loop
// It is useful for when we need to do manual movement outside
// of the physics world. We need to take time into account for smooth
// animations.
override func update(_ currentTime: TimeInterval) {
}
Contact detection
To detect contacts between objects, we added the SKPhysicsContactDelegate
protocol to the class declaration. Here is the function we need to add to implement the protocol.
// This function is called if any two objects touch each other
func didBegin(_ contact: SKPhysicsContact) {
}
didBegin(contact:)
will be called for each pair of objects that touch each other.
Reacting to finger taps
To detect taps, we are provided with these functions to override.
// This function is called if a finger is put on the screen
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
// This function is called if a finger is lifted off the screen
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
The first one will be called when any finger is first tapped on the scene. The second will be called if any finger is lifted off the screen. You might need to process a set of touches at a time if they are simultaneous.
Now, we're ready to build up the game.
Use SpriteKit Extensions to make it easier
In previous articles, I shared extensions for SKPhysicsBody and CGVector. I will be using them in the code in the rest of this article.
Use stubs to help get the code structure right
When I build up a game, I like to make stub functions to get the basic structure in. So, let's start with this. Add these function calls to startGame()
:
func startGame() {
// We will call this after the game is over to start again, so
// start by removing all of the nodes so we have a blank scene
self.removeAllChildren()
setUpPhysicsWorld()
createBall()
createWalls()
createPassedBallDetectors()
createPaddles()
createScore()
resetBall()
}
And then just make stubs so that the game code compiles without errors.
func setUpPhysicsWorld() {
}
func createBall() {
}
func resetBall() {
}
func createWalls() {
}
func createPassedBallDetectors() {
}
func createPaddles() {
}
func createScore() {
}
And, we need some properties to keep track of the game state, so add this to the top of the scene class:
var ball: SKShapeNode?
var topPaddle: SKShapeNode?
var bottomPaddle: SKShapeNode?
var topBallDetector: SKShapeNode?
var bottomBallDetector: SKShapeNode?
var topScore: SKLabelNode?
var bottomScore: SKLabelNode?
And, we're going to use these constants when we make our nodes.
let ballRadius: CGFloat = 10
let paddleSize = CGSize(width: 100, height: 10)
let paddleEdgeOffset: CGFloat = 60
let wallWidth: CGFloat = 10
All of the coordinates and values in SpriteKit come from CoreGraphics, which is why they use the CG
prefixed types. We will also use CGPoint
and CGVector
. If you don't specify the types, Swift will guess that you want Int
or Double
and then you'll have to cast the values to use them in SpriteKit.
Let's flesh it out a little
Setting up the physics world is easy. We just need to set up gravity and set the contact delegate so that our scene is notified when two objects touch each other.
func setUpPhysicsWorld() {
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
self.physicsWorld.contactDelegate = self
}
We use a vector of (0, 0)
because we don't want any gravity.
All of the creation functions will create a shape, set up a physics body for it, and then position it.
Remember that the SpriteKit coordinate system has the origin in the bottom left. And the scene has a size
property that you can use to position objects in it. Finally, the position of an object refers to the center point of the object.
Now, let's put in the side walls. Here's a function that makes a full height wall at a given x
.
func createVerticalWall(x: CGFloat) {
let wallSize = CGSize(width: wallWidth, height: size.height)
let wall = SKShapeNode(rectOf: wallSize)
wall.physicsBody =
SKPhysicsBody(rectangleOf: wallSize)
.ideal()
.manualMovement()
wall.position = CGPoint(x: x, y: size.height/2)
wall.strokeColor = .white
wall.fillColor = .white
addChild(wall)
}
And then we just call it twice. Once for the left side and once for the right.
func createWalls() {
createVerticalWall(x: wallWidth/2)
createVerticalWall(x: size.width - wallWidth/2)
}
We can do something similar for the paddles, except passing in y
and returning the paddle.
func createPaddle(y: CGFloat) -> SKShapeNode {
let paddle = SKShapeNode(rectOf: paddleSize)
paddle.physicsBody =
SKPhysicsBody(rectangleOf: paddleSize)
.ideal()
.manualMovement()
paddle.position = CGPoint(x: size.width/2, y: y)
paddle.strokeColor = .white
paddle.fillColor = .white
addChild(paddle)
return paddle
}
We use it like this:
func createPaddles() {
self.topPaddle = createPaddle(y: size.height - paddleEdgeOffset)
self.bottomPaddle = createPaddle(y: paddleEdgeOffset)
}
We need to keep track of paddles in properties because other functions we'll make need to access them.
Creating a ball is similar, but we won't position it. We'll leave that to the resetBall
function
func createBall() {
let ball = SKShapeNode(circleOfRadius: ballRadius)
ball.position = CGPoint(x: size.width/2, y: size.height/2)
ball.physicsBody =
SKPhysicsBody(circleOfRadius: ballRadius)
.ideal()
ball.strokeColor = .white
ball.fillColor = .white
addChild(ball)
self.ball = ball
}
And, finally, resetBall()
will give it an initial velocity.
func resetBall() {
ball?.physicsBody?.velocity = CGVector(dx: 200, dy: 200)
}
Running this code, you'll get this result
Subscribe to be notified of the next SpriteKit article
We'll continue this in the next article where we'll move the paddles based on finger taps.
Make sure to sign up to my newsletter to get notified when a new article is published.
Contact me if you are working on a game and need help.
Next Article: How to Make Pong in SpriteKit Part II: Touch Events