SpriteKit with Swift: Orienting Sprites to a Location

About this project: This project uses Xcode’s SpriteKit Game template to practice moving sprites in response to touches. It was created in Xcode v6.1.1 using the Swift programming language.

Getting Started

Open Xcode and create a new project (⇧ + ⌘ + N) for an iOS Game.

New iOS Game
New iOS Game

In this example, we’re calling the project DemoSpriteOrientation and we’re making it for iPad using Swift. Ensure SpriteKit is selected for the Game Technology option.

SpriteKit Project for iPad
SpriteKit Project for iPad

The first thing we’re going to do is click on GameScene.swift and remove everything inside the touchesBegan method so that it looks like this:

//
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    // Called when a touch begins
}
//

Build and run the project, and you should see a gray screen with the “Hello, World” label in the middle.

Setting the Scene

Let’s add a sprite to the scene. This was previously handled inside touchesBegan, but we want the scene to contain a sprite on load. First, add a sprite property to the GameScene class:

//
class GameScene: SKScene {
    var sprite: SKSpriteNode?
//

Now replace the contents of didMoveToView:

//
override func didMoveToView(view: SKView) {
    // Create space ship sprite.
    sprite = SKSpriteNode(imageNamed: "Spaceship")
    sprite!.xScale = 0.5
    sprite!.yScale = 0.5

    // Give the sprite an initial location in the center of the screen.
    sprite!.position = CGPointMake(frame.width / 2, frame.height / 2)

    // Add the sprite to the scene.
    self.addChild(sprite!)
}
//

Build and run the app. You should now see your space ship in the middle of the screen upon loading the scene.

iPad Simulator View
iPad Simulator View

That’s it for the initial setup.

Responding to Touches

Upon touching the screen, we want the space ship to perform two actions. First, it should rotate so that it faces its direction of travel. Second, it should move to the location on the screen that was touched. Since rotation is trickier, we’re going to start by telling the ship how to move.

Inside the touchesBegan method, add the following code:

//
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    // Capture the touch event.
    if let touch:UITouch = touches.first as? UITouch {

        // Get the position that was touched.
        let touchPosition = touch.locationInNode(self)

        // Define actions for the ship to take.
        let moveAction = SKAction.moveTo(touchPosition, duration: 0.5)

        // Tell the ship to execute actions.
        sprite!.runAction(moveAction) 
    }
}
//

Note: The code snippet above has changed as of 6/22/2015 (due to a reader pointing out conflicts in a new version of Swift. In the original version of this code, line 4 was a single line of code (not a block), which read: let touch = touches.anyObject() as UITouch. I have not tested whether the new version works yet. Thanks Paul for pointing out the conflict since the most recent Swift update.

This is all you need to tell the sprite to move to a designated point. To elaborate on what’s happening here:

  • touches.anyObject() as UITouch is capturing the touch and casting it as a UITouch object.
  • touch.locationInNode(self) is fetching a CGPoint containing the x and y coordinates of the touch event within the SKScene (a.k.a. “self”).
  • SKAction.moveTo is a method that takes two parameters:
    1. A CGPoint containing the x and y coordinates of a location. We’re passing the location of the touch event.
    2. An NSTimeInterval Double within which the action should take place. We’re passing a brisk move-speed of 0.5.
  • runAction(moveAction) is telling the sprite to execute whatever action is stored in moveAction.

Build and run the app. Your ship should now move around to wherever you touch on screen, but there is a problem. It’s always facing up! That’s not very natural-looking travel for a space ship, so let’s fix it.

Orienting the Sprite

This is the challenging part. Where do we even start? Well, we’re going to have to rotate the ship somehow. Let’s take a look back at a couple lines that used to be in the touchesBegan method.

//
let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)

sprite.runAction(SKAction.repeatActionForever(action))
//

Ok, so from looking at this we know how to rotate the ship. SKAction.rotateByAngle is very similar to SKAction.moveTo, in that it takes two parameters and stores the resulting action to be used later. The difference is that in the case of rotateByAngle, the first parameter is a CGFloat for the radians by which to rotate. Now the challenge is finding the angle in radians.

Where to begin? Start with what we know.

  1. We know the sprite is positioned on a CGPoint, which has x and y coordinates.
  2. We know the touch occurs at a CGPoint, which has x and y coordinates.
  3. With those two points, we can imagine a right triangle.

triangle

The next step is to have a passing knowledge of Geometry, or — if you’re like me and haven’t looked at a Math textbook since high school — the next step is to hit Google. My myriad searches included such inspired Google-fu as “xcode swift rotate sprite to face point” and “xcode swift orienting sprites,” which led to the perusal of numerous Stack Overflow posts, which led to me having a vague certainty that, somehow, the tangent was important. Finally, I stumbled upon this wenda.io post.

The magic all happens in a little function called atan2. You can find a description of this function here. In short, it takes two parameters for the x and y coordinates and spits out the arc (or inverse) tangent measured in radians. Remember rotateByAngle‘s method definition?

//
class func rotateByAngle(_ radians: CGFloat,
    duration sec: NSTimeInterval) -> SKAction
//

Radians is what we’ve been looking for.

Following the example in the wenda.io post, let’s pass the values we need into atan2 by finding the difference between our ship’s starting and ending points. Change touchesBegan to include the following:

//
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    /* Called when a touch begins */
    // Capture the touch event.
    let touch = touches.anyObject() as UITouch

    // Get the position that was touched (a.k.a. ending point).
    let touchPosition = touch.locationInNode(self)

    // Get sprite's current position (a.k.a. starting point).
    let currentPosition = sprite!.position

    // Calculate the angle using the relative positions of the sprite and touch.
    let angle = atan2(currentPosition.y - touchPosition.y, currentPosition.x - touchPosition.x)

    // Define actions for the ship to take.
    let rotateAction = SKAction.rotateByAngle(angle, duration: 0.0)
    let moveAction = SKAction.moveTo(touchPosition, duration: 0.5)

    // Tell the ship to execute actions.
    sprite!.runAction(SKAction.sequence([rotateAction, moveAction]))
}
//

What you’ve just done:

  • sprite!.position gets the CGPoint location of the ship. We’re only doing this so the syntax inside atan2 is a little simpler.
  • atan2 is taking a y value that is the difference between the starting and ending y values, and an x value that is the difference between the starting and ending x values.
    Note: Pay close attention to the order in which the parameters must be passed into atan2(y, x).
  • SKAction.rotateByAngle is going to rotate our ship by the angle returned by atan2, and should do so instantly.
  • Finally, we’ve modified our runAction statement to include an SKAction.sequence, which takes an array of SKActions and executes them in order.

Build and run the app!

Oops

This isn’t what we want at all. Taking another look at the oft-referenced wenda.io post post, I noticed the angle from atan2 was being passed into a different SKAction. The one we want to use is SKAction.rotateToAngle.

To my shame, at the time of this writing I still haven’t investigated exactly why these two methods behave so differently with the same input. I will update this post when I know more. For now, change your rotateAction to use the proper action:

//
let rotateAction = SKAction.rotateToAngle(angle, duration: 0.0)
//

There, that should do it. Build and run again.

Hmm…

Something still isn’t right. Our ship is flying sideways. Why? It turns out the answer is so stupidly simple you’ll be glad you didn’t have to spend any real time wracking your brain about it like I did. It has to do with the base orientation of the ship. Since it seems to point its side in the direction we want its nose to be pointing, all we really have to do is slightly increase the angle to which it’s rotating.

Modify your rotateAction variable to add 90 degrees in radians to the rotation (a.k.a. π/2):

//
let rotateAction = SKAction.rotateToAngle(angle + CGFloat(M_PI*0.5), duration: 0.0)
//

It Works!

Congratulations! Now you know how to rotate a sprite so that it faces any point on the screen. Here are just a few possible applications for this very simple code:

  • Use it in touchesMoved to make a sprite change its direction in real time as you drag your finger on the screen.
  • Use it to make your hero face an enemy who has appeared on screen.
  • Use it to make projectiles like missiles or arrows face the direction they’re fired in.
  • Use it to create a “heat-seeking” projectile that always aims at the position of another sprite.

If you’re still having difficulty…

Here is the full code for the GameScene.swift file. This is all you need for the project to run as intended:

//
import SpriteKit

class GameScene: SKScene {

    var sprite: SKSpriteNode?

    override func didMoveToView(view: SKView) {
        // Create space ship sprite.
        sprite = SKSpriteNode(imageNamed: "Spaceship")
        sprite!.xScale = 0.5
        sprite!.yScale = 0.5

        // Give the sprite and initial location in the center of the screen.
        sprite!.position = CGPointMake(frame.width / 2, frame.height / 2)

        // Add the sprite and label to the scene.
        self.addChild(sprite!)
    }

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        /* Called when a touch begins */
        // Capture the touch event.
        let touch = touches.anyObject() as UITouch

        // Get the position that was touched (a.k.a. ending point).
        let touchPosition = touch.locationInNode(self)

        // Get sprite's current position (a.k.a. starting point).
        let currentPosition = sprite!.position

        // Calculate the angle using the relative positions of the sprite and touch.
        let angle = atan2(currentPosition.y - touchPosition.y, currentPosition.x - touchPosition.x)

        // Define actions for the ship to take.
        let rotateAction = SKAction.rotateToAngle(angle + CGFloat(M_PI*0.5), duration: 0.0)
        let moveAction = SKAction.moveTo(touchPosition, duration: 0.5)

        // Tell the ship to execute actions.
        sprite!.runAction(SKAction.sequence([rotateAction, moveAction]))

    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    }
}
//
Advertisements

2 thoughts on “SpriteKit with Swift: Orienting Sprites to a Location

  1. I had to change, “let touch = touches.anyObject() as UITouch” to “if let touch:UITouch=touches.first as? UITouch{ …”.
    Thanks for this great tutorial.
    To be honest, it is one of the best I have come across!

    Paul

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s