SpriteKit Smooth Rotation
Making a sprite rotate towards a certain direction is very easy in SpriteKit.
In this short tutorial I will show how to make a sprite rotate smoothly towards a point that you touch on the screen. In addition the sprite will move towards the location.
This tutorial is written for Swift 3.
Getting Started
Launch Xcode and create a new SpriteKit game from the template picker.
Open GameScene.sks
Delete the label from the scene.
Set the scene size to iPhone 6S plus.
In GameScene.swift delete all the template code. Your file should look like this:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
}
Archer Sprite
You need to add an image file by dragging one into Xcode. You can use my archer sprite if you like. Download
Character Class
We are going to make a subclass of SKSpriteNode so that some movement information can be stored with the sprite. This could be stored in the gamescene but by subclassing we could easily reuse the class to make more moveable sprites.
After the import statements and before the GameScene class add this:
class Character: SKSpriteNode {
let movePointsPerSecond: CGFloat = 150.0
var velocity = CGVector(dx: 0.0, dy: 0.0)
var isMoving = false
}
This creates a subclass of SKSpriteNode called Character. The Character class will contain the speed we want the sprite to move per second, the current velocity (the direction and speed of movement), and a boolean which determines if the character is moving or not.
We’re only going to create one character in this tutorial – an archer. But the Character class can be reused for other rotating and movable sprites.
GameScene
In the GameScene class add the following code:
var lastUpdateTime: CFTimeInterval = 0 // Track the delta time
var archer = Character(imageNamed: "archer")
override func didMove(to view: SKView) {
let position = CGPoint(x: 0, y: 0)
archer.position = position
addChild(archer)
}
The lastUpdateTime variable is used later to ensure our archer is moved smoothly.
Then we create an instance of the archer and in the didMoveToMove method we add him to scene. Our scene has an anchor point of x: 0.5 and y: 0.5 so the archer will be positioned in the centre of the scene.
Rotation And Movement
When we tap a position on the screen, we want the archer to rotate to that position and move towards it at the speed we set in the Character class (movePointsPerSecond).
The function below takes a character object and a position and runs a SKAction to rotate the sprite to face towards that position. It then sets the velocity of the sprite, which is its direction multiplied by the amount we want to move the sprite per second.
func moveAndRotate(sprite: Character, toPosition position: CGPoint) {
// Use the atan2 function to determine the angle the sprite should rotate to.
let angle = atan2(position.y - sprite.position.y, position.x - sprite.position.x)
// Setup the rotate action using pi to convert the angle into radians
// for the action.
let rotateAction = SKAction.rotate(toAngle: angle - .pi / 2, duration: 0.05, shortestUnitArc:true)
// Run the action on the sprite.
sprite.run(rotateAction)
// Calculate the offset by subtracting the sprite's position from
// from the position the sprite should move to.
let offset = position - sprite.position
// Normalize means to convert the offset to a length of 1
// Se we can then apply any distance to it.
// We store this value in direction
let direction: CGPoint = offset.normalized()
// Finally we store this in the sprite's velocity property.
// We don't actually move the sprite here, that will be done
// in the update method.
sprite.velocity = CGVector(dx: direction.x * sprite.movePointsPerSecond, dy: direction.y * sprite.movePointsPerSecond)
}
Update
Add the update method as shown below.
override func update(_ currentTime: TimeInterval) {
// Clamp deltaTime to a maximum of 1/30th of a second
let deltaTime = max(1.0/30, currentTime - lastUpdateTime)
lastUpdateTime = currentTime
update(character: archer, dt: deltaTime)
}
The update method is called roughly 60 times per second. But this number is not guaranteed. It could be out by a millisecond here or there.
To make sure our sprites move smoothly, we calculate the difference in time since the update method was last called.
This difference is stored in the deltaTime variable and passed to our next update method shown below.
func update(character: Character, dt: CFTimeInterval) {
if character.isMoving == true {
let newX = character.position.x + character.velocity.dx * CGFloat(dt)
let newY = character.position.y + character.velocity.dy * CGFloat(dt)
character.position = CGPoint(x: newX, y: newY)
}
}
Because its likely that other characters other than the archer, will move and rotate, this function takes the character and the deltaTime from the update method and determines the new position of the character. It then updates the character’s position on screen.
Touch Methods
The last code we need to add is our touch handling methods. We need to use three methods:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location(in: self)
archer.isMoving = true
moveAndRotate(sprite: archer, toPosition: touchLocation)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location(in: self)
moveAndRotate(sprite: archer, toPosition: touchLocation)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
archer.isMoving = false
}
touchesBegan simply sets the isMoving property of the archer to true so that the update method knows that this sprite should move.
touchesBegan and touchesMoved register the location of the touch on the screen and pass that to the moveAndRotateSprite method along with the sprite to move. In this simple example, this is just the archer.
Finally, touchesEnded sets the isMoving property of the archer to false. The update method will no longer move the sprite.
I hope you find this tutorial useful.