Setting A Restore Point For A Character In SpriteKit
If you are making a game with long or difficult levels, it can be a good idea to have your player spawn at a location other than the start of the level if the game is lost. It may be better for gameplay or to provide an incentive to continue with difficult levels.
Here’s one way to do it.
In this example, our witch is flying along the countryside, enjoying the view when suddenly she collides with a bomb, ending the game! Instead of sending the witch back to the beginning of the level, we want to restart the scene with the player restored to the last milestone they reached.
How it will look
This is how the tutorial will look when it is finished. The red bar is visible in this video, but is hidden when the game is run if you follow along.
What we’re going to do
- Add one or more marker sprites to act as restore points, these are visible in the scene editor but not displayed when the game is playing.
- Test for contact between the player and a marker. If the player touches a marker, the current position of the player is stored.
- If the player dies and the level is restarted, check to see if there is a stored position and if so, restart the level with the player at that position.
Let’s start with some code
- Create a new Xcode project using iOS Game as the template.
- Click Next, give the project a name.
- Click Next and save the project on your Mac.
Open up GameScene.swift
Delete all the unnecessary code from GameScene so that it looks like this:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
After the import statements and before the GameScene class add the following code. These would normally go in their own files but for this simple tutorial the GameScene file is fine.
// These are the physics categories so we can check which
// objects have made contact.
struct PhysicsCategory {
static let None : UInt32 = 0
static let All : UInt32 = UInt32.max
static let Player : UInt32 = 0x1 << 1
static let Bomb : UInt32 = 0x1 << 2
static let RestorePoint : UInt32 = 0x1 << 3
}
// This struct is available globablly. Here we are just using
// it to store the lastRestorePoint position.
struct Game {
static var lastRestorePoint: CGPoint?
}
// The RestorePoint class sets up its physics information
// and hides itelf when the game is playing.
class RestorePoint: SKSpriteNode {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isHidden = true
physicsBody?.categoryBitMask = PhysicsCategory.RestorePoint
physicsBody?.contactTestBitMask = PhysicsCategory.Player
physicsBody?.collisionBitMask = PhysicsCategory.None
physicsBody?.isDynamic = false
physicsBody?.affectedByGravity = false
}
}
// The Bomb class sets up its physics information.
class Bomb: SKSpriteNode {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
physicsBody?.categoryBitMask = PhysicsCategory.Bomb
physicsBody?.contactTestBitMask = PhysicsCategory.Player
physicsBody?.collisionBitMask = PhysicsCategory.None
physicsBody?.isDynamic = false
physicsBody?.affectedByGravity = false
}
}
Physics properties for each object can also be set in the Xcode Scene Editor but I prefer to give each object a class and define the properties there. Also, it’s likely that in a game, each object would have its own class anyway for additional functionality.
Setup the player sprite and physics
The player sprite will be added and positioned in the scene editor, but in GameScene.swift we need to create a variable to hold on to the sprite. Add this just before the didMoveToView method:
var player: SKSpriteNode!
We need the game scene to respond to physics contacts so we tell it to conform to SKPhysicsContactDelegate. This means that when two objects physics bodies make contact, SpriteKit will call some functions in our scene. We also need to tell physicsWorld that our scene is the delegate for contacts.
Inside the didMoveToView method, after calling super add this line:
physicsWorld.contactDelegate = self
We will connect our player sprite variable to the sprite in the scene editor:
player = childNode(withName: "Player") as! SKSpriteNode
And define the physics properties of our player:
player.physicsBody?.categoryBitMask = PhysicsCategory.Player
player.physicsBody?.contactTestBitMask = PhysicsCategory.RestorePoint
player.physicsBody?.collisionBitMask = PhysicsCategory.None
Now we are going to check our lastRestorePoint variable. If this variable has a value we will position our player at the location it contains. If there is no value it means either this is the first time playing, or the player has just died without reaching a restore point.
if let lastRestorePoint = Game.lastRestorePoint {
player.position = lastRestorePoint
}
Your didMoveToView method should like something like this. I have added additional comments to the example below:
override func didMove(to view: SKView) {
super.didMove(to: view)
// We set the contactDelegate of the physics world to the game scene
// so that we can check when objects make contact with each other.
physicsWorld.contactDelegate = self
// Get the Player sprite from the scene and store in the player
// variable.
player = childNode(withName: "Player") as! SKSpriteNode
// Define the physics properties of the player sprite.
player.physicsBody?.categoryBitMask = PhysicsCategory.Player
player.physicsBody?.contactTestBitMask = PhysicsCategory.RestorePoint
player.physicsBody?.collisionBitMask = PhysicsCategory.None
// Check the lastRestorePoint
if let lastRestorePoint = Game.lastRestorePoint {
player.position = lastRestorePoint
} else {
// The lastRestorePoint variable is not set. Either
// this is the first time playing or the player has
// died before reaching a restore point.
}
}
Reacting to physics contacts
We’re almost done with our code. What we want to do is have SpriteKit notify us when two objects make contact. We can do this using the didBeginContact method. This is called because we told the physicsWorld that our scene is the delegate for contacts.
Add this below the didMoveToView method:
func didBegin(_ contact: SKPhysicsContact) {
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
}
Each object is assigned to one of the variables. We want to perform a test to ensure that the lowest category object is stored in the first body. This will make it easier to check for contacts. We defined the categories in the PhysicsCategory struct earlier.
Add the following just after the code above, but still within the didBeginContact method:
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
This just forces the lowest category physics object into the first body.
Setting the restore point
Now we are going to check if the player has made contact with a restore point. If so, the position of the player will be stored in our lastRestorePoint global variable from the Game struct.
Add the following code within the didBeginContact method:
if firstBody.categoryBitMask == PhysicsCategory.Player && secondBody.categoryBitMask == PhysicsCategory.RestorePoint {
Game.lastRestorePoint = player.position
}
We will perform one final check in this method. Add the following code to check if the player has made contact with the bomb. If this is true the game ends. We create a transition and ask the view to present a new scene using the transition.
if firstBody.categoryBitMask == PhysicsCategory.Player && secondBody.categoryBitMask == PhysicsCategory.Bomb {
let transition = SKTransition.doorway(withDuration: 0.5)
if let gameScene = SKScene(fileNamed: "GameScene") {
self.view?.presentScene(gameScene, transition: transition)
}
}
Your didBeginContact method should now look like this:
func didBegin(_ contact: SKPhysicsContact) {
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
// Ensure lowest category is always stored in the first body.
if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
firstBody = contact.bodyA
secondBody = contact.bodyB
} else {
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if firstBody.categoryBitMask == PhysicsCategory.Player && secondBody.categoryBitMask == PhysicsCategory.RestorePoint {
Game.lastRestorePoint = player.position
}
if firstBody.categoryBitMask == PhysicsCategory.Player && secondBody.categoryBitMask == PhysicsCategory.Bomb {
let transition = SKTransition.doorway(withDuration: 0.5)
if let gameScene = SKScene(fileNamed: "GameScene") {
self.view?.presentScene(gameScene, transition: transition)
}
}
}
Almost there…
We’re almost done with our code. We just need to add the following. In the deinit function, we just print out a message to confirm that the scene has been deallocated from memory before a new scene is presented.
We want the camera to follow our witch as she flies, the final code in the update function does just that.
deinit {
print("The previous game scene has been deinited👍")
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if let camera = self.camera {
camera.position.x = player.position.x
}
player.physicsBody?.applyForce(CGVector(dx: 20, dy: 0))
}
That’s all the code we need for this example. On the next page you will setup the scene in the Xcode scene editor.