Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 1/2

$
0
0
Learn Trigonometry for game programming!

Learn Trigonometry for game programming!

Update Note: This is the third incarnation of one of our very popular tutorials – the first version was written by Tutorial Team member Matthijs Hollemans for Cocos2D, and the second version was update to Sprite Kit by Tony Dahbura. This latest version still uses Sprite Kit, but is updated for iOS 8 and Swift.

Does the thought of doing mathematics give you cold sweats? Are you ready to give up on your career as a budding game developer because the math just doesn’t make any sense to you?

Don’t fret – math can be fun, and this cool 2-part game tutorial will prove it!

Here’s a little secret: as an app developer, you don’t really need to know a lot of math. Most of the computations that we do in our professional lives don’t go much beyond basic arithmetic.

That said, for making games it is useful to have a few more math skills in your toolbox. You don’t need to become as smart as Archimedes or Isaac Newton, but a basic understanding of trigonometry, combined with some common sense, will take you a long way.

In this tutorial, you will learn about some important trigonometric functions and how you can use them in your games. Then you’ll get some practice applying the theories by developing a simple space shooter iPhone game using the Sprite Kit game framework.

Don’t worry if you’ve never used Sprite Kit before or are going to use a different framework for your game – the mathematics covered in this tutorial are applicable to any engine you might choose to use. And you don’t need any prior experience, as I’ll walk through the process step-by-step.

If you supply the common sense, this tutorial will get you up to speed on the trigonometry, so let’s get started!

Note: The game you’ll build in this tutorial uses the accelerometer so you’ll need a real iOS device and a paid developer account.

Getting Started: It’s All About Triangles

It sounds like a mouthful, but trigonometry (or trig, for short) simply means calculations with triangles (that’s where the tri comes from).

You may not have realized this, but games are full of triangles. For example, imagine you have a spaceship game, and you want to calculate the distance between these ships:

Distance between ships

You have X and Y position of each ship, but how can you find the length of that line?

Well, you can simply draw a line from the center point of each ship to form a triangle like this:

Then, since you know the X and Y coordinates of each ship, you can compute the length of each of the new lines. Now that you know the lengths of two sides of the triangle, you can use trig to compute the length of the diagonal line – the distance between the ships.

Note that one of the corners of this triangle has an angle of 90 degrees. This is known as a right triangle (or right-angle triangle, for you Brits out there!), and that’s the sort of triangle you’ll be dealing with in this tutorial.

Any time you can express something in your game as a triangle with a 90-degree right angle – such as the spatial relationship between the two sprites in the picture – you can use trigonometric functions to do calculations on them.

So in summary, trigonometry is the mathematics that you use to calculate the lengths and angles of right triangles. And that comes in handy more often that you might think.

For example, in this spaceship game you might want to:

  • Have one ship shoot a laser in the direction of the other ship
  • Have one ship start moving in the direction of another ship to chase
  • Play a warning sound effect if an enemy ship is getting too close

All of this and more you can do with the power of trigonometry!

Your Arsenal of Functions

First, let’s get the theory out of the way. Don’t worry, I’ll keep it short so you can get to the fun coding bits as quickly as possible.

These are the parts that make up a right triangle:

In the picture above, the slanted side is called the hypotenuse. It always sits across from the corner with the 90-degree angle (also called a right angle), and it is always the longest of the three sides.

The two remaining sides are called the adjacent and the opposite, as seen from one particular corner of the triangle, the bottom-left corner in this case.

If you look at the triangle from the point of view of the other corner (top-right), then the adjacent and opposite sides switch places:

Alpha (α) and beta (β) are the names of the two other angles. You can call these angles anything you want (as long as it sounds Greek!), but usually alpha is the angle in the corner of interest and beta is the angle in the opposing corner. In other words, you label your opposite and adjacent sides with respect to alpha.

The cool thing is that if you only know two of these things, trigonometry allows you to find out all the others using the trigonometric functions sine, cosine and tangent. For example, if you know any angle and the length of one of the sides, then you can easily derive the lengths and angles of the other sides and corners:

You can see the sine, cosine, and tangent functions (often shortened to sin, cos and tan) are just ratios – again, if you know alpha and the length of one of the sides, then sin, cos and tan are ratios that relate two sides and the angle together.

Think of the sin, cos and tan functions as “black boxes” – you plug in numbers and get back results. They are standard library functions, available in almost every programming language, including Swift.

Note: The behavior of the trigonometric functions can be explained in terms of projecting circles onto straight lines, but you don’t need to know how to derive those functions in order to use them. If you’re curious, there are plenty of sites and videos to explain the details; check out the Math is Fun site for one example.

Know Angle and Length, Need Sides

Let’s consider an example. Suppose that you know the alpha angle between the ships is 45 degrees, and the length between the ships (the hypotenuse) is 10 points long.

Triangles-in-games-measured

You can then plug these values into the formula:

sin(45) = opposite / 10

To solve this for the hypotenuse, you simply shift this formula around a bit:

opposite = sin(45) * 10

The sine of 45 degrees is 0.707 (rounded to three decimal places), and filling that in the forumla gives you the result:

opposite = 0.707 * 10 = 7.07

There is a handy mnemonic for remembering what these functions do that you may remember from high school: SOH-CAH-TOA (where SOH stands for Sine is Opposite over Hypotenuse, and so on), or if you need something more catchy: Some Old Hippy / Caught Another Hippy / Tripping On Acid. (That hippy was probably a mathematician who did a little too much trig! :])

Know 2 Sides, Need Angle

The formulae above are useful when you already know an angle, but that is not always the case – sometimes you know the length of two sides and are looking for the angle between them. To derive the angle, you can use the inverse trig functions, aka arc functions (which has nothing to do with Automatic Reference Counting, before you ask!):

Inverse trig functions

  • angle = arcsin(opposite/hypotenuse)
  • angle = arccos(adjacent/hypotenuse)
  • angle = arctan(opposite/adjacent)

If sin(a) = b, then it is also true that arcsin(b) = a. Of these inverse functions, you will probably use the arc tangent (arctan) the most in practice because it will help you find the hypotenuse (remember TOA – Opposite over Adjacant!). Sometimes these functions are written as sin-1, cos-1, and tan-1, so don’t let that confuse you.

Is any of this sinking in or sounding familiar? Good, because you’re not done yet with the theory lesson – there is still more that you can calculate with triangles.

Know 2 Sides, Need Remaining Side

Sometimes you may know the length of two of the sides and you need to know the length of the third (as with the example at the beginning of this tutorial, where you wanted to find the distance between the two spaceships).

This is where Trigonometry’s Pythagorean Theorem comes to the rescue. Even if you forgot everything else about math, this is probably the one formula you do remember:

a2 + b2 = c2

Or, put in terms of the triangle that you saw earlier:

opposite2 + adjacent2 = hypotenuse2

If you know any two sides, calculating the third is simply a matter of filling in the formula and taking the square root. This is a very common thing to do in games and you’ll be seeing it several times in this tutorial.

Note: Want to drill this formula into your head while having a great laugh at the same time? Search YouTube for “Pythagoras song” – it’s an inspiration for many!

Have Angle, Need Other Angle

Lastly, the angles. If you know one of the non-right angles from the triangle, then figuring out the other ones is a piece of cake. In a triangle, all angles always add up to a total of 180 degrees. Because this is a right triangle, you already know that one of the angles is 90 degrees. That leaves:

alpha + beta + 90 = 180

Or simply:

alpha + beta = 90

The remaining two angles must add up to 90 degrees. So if you know alpha, you can calculate beta, and vice-versa.

And those are all the formulae you need to know! Which one to use in practice depends on the pieces that you already have. Usually you either have the angle and the length of at least one of the sides, or you don’t have the angle but you do have two of the sides.

Enough theory. Let’s put this stuff into practice.

To Skip, or Not to Skip?

In the next few sections, you will be setting up a basic Sprite Kit project with a spaceship that can move around the screen using the accelerometer. This won’t involve any trigonometry (yet), so if you already know Sprite Kit and feel like this guy:

"F that!" guy

Then feel free to skip ahead to the Begin the Trigonometry! section below – I have a starter project waiting for you there.

But if you’re the type who likes to code everything from scratch, keep reading! :]

Creating the Project

First make sure you have Xcode 6.1.1 or later, as Swift is a brand new language and the syntax is prone to change subtly between versions.

Next, start up Xcode, select File\New\Project…, choose iOS\Application\Game template. Name the project TrigBlaster. Make sure Language is set to “Swift”, Game Technology is set to “SpriteKit”, and Devices is set to “iPhone”. Then click Next:

Select SpriteKit Template

Build and run the template project. If all works OK, you should see the following:

SpriteKitHelloWorld

Download the resources for this tutorial. This file contains the images for the sprites and the sound effects. Unzip it, and drag the images individually into your Images.xcassets to set up the sprites. You can delete/replace the Spaceship sprite that comes with the default project template, as you won’t be using that.

Now add the sounds. You can simply drag the whole Sounds folder into Xcode, but make sure you select the “Create groups” option.

AddingSounds

FileImport

Great; the usual preliminaries are over with – now let’s get coding!

Steering with Accelerometers

Because this is a simple game, you will be doing most of your work inside a single file: GameScene.swift. Right now, this file contains a bunch of stuff that you don’t need. It also does not run with the correct orientation for the game, so let’s fix that first.

Switching to Landscape Orientation

Open the target settings by clicking your TrigBlaster project in the Project Navigator and selecting the TrigBlaster target. Then, in the Deployment Info section make sure General is checked at the top and under Device Orientation uncheck all but Landscape Left (as shown below):

LandscapeRight

If you build and run, the app will now launch in landscape orientation. The app is currently loading an empty scene from the GameScene.sks file in GameViewController.swift, and then in GameScene.swift, a “Hello World” label is being added programmatically.

Replace the contents of GameScene.swift with:

import SpriteKit
 
class GameScene: SKScene {
 
  override func didMoveToView(view: SKView) {
 
    // set scene size to match view
    size = view.bounds.size
 
    backgroundColor = SKColor(red: 94.0/255, green: 63.0/255, blue: 107.0/255, alpha: 1)
  }
 
  override func update(currentTime: CFTimeInterval) {
 
  }
}

Build and run, and you should see… nothing but purple:

Purple

Let’s make things a bit more exciting by adding a spaceship to the scene. Modify the GameScene class as follows:

class GameScene: SKScene {
 
  let playerSprite = SKSpriteNode(imageNamed: "Player")
 
  override func didMoveToView(view: SKView) {
 
    // set scene size to match view
    size = view.bounds.size
 
    backgroundColor = SKColor(red: 94.0/255, green: 63.0/255, blue: 107.0/255, alpha: 1)
 
    playerSprite.position = CGPoint(x: size.width - 50, y: 60)
    addChild(playerSprite)
  }
 
  ...
}

This is all pretty basic if you have worked with Sprite Kit before. The playerSprite property holds the spaceship sprite, which is positioned in the bottom-right corner of the screen. Remember that with Sprite Kit it is the bottom of the screen that has a Y-coordinate of 0, unlike in UIKit where y = 0 points to the top of the screen. You’ve set the Y-coordinate to 60 to position it just above the FPS (Frames Per Second) counter in the bottom-left corner.

Note: The FPS counter is useful for debugging purposes, but you can disable it in GameViewController.swift if it annoys you. You’ll probably want to do that before you submit your game to the App Store!

Build and run to try it out, and you should see the following:

PurpleShip

To move the spaceship, you’ll be using the iPhone’s built-in accelerometer. Unfortunately, the iOS Simulator can’t simulate the accelerometer, so from now on you will need to run the app on a real device to test it.

Note: If you are unsure how to install the app on a device, check out this extensive tutorial that explains how to obtain and install the certificates and provisioning profiles that allow Xcode to install on a physical iPhone or iPad. It’s not as intimidating as it looks, but you will need to sign up for the paid Apple developer program.

To move the spaceship with the accelerometer, you’ll need to tilt your device from side to side. This was the reason you de-selected all Device Orientation options except for Landscape Right in the Project Settings screen earlier, because it would be really annoying for the screen to flip when you’re in the middle of a heated battle!

Using the accelerometer is pretty straightforward thanks to the Core Motion framework. There are two ways to get accelerometer data: You can either register to have it delivered to your application at a specific frequency via a callback, or you can poll the values when you need them. Apple recommends not having data pushed to your application unless timing is very critical (like a measurement or navigation service) because it can drain the batteries more quickly.

Your game already has a logical place from which to poll the accelerometer data: the update() method that gets called by Sprite Kit once per frame. You will read the accelerometer values whenever this method is fired, and use them to move the spaceship.

First, add the following import to the top of GameScene.swift:

import CoreMotion

Now you’ll have Core Motion available to you and it’ll be linked into your app.

Next, add the following properties inside the class implementation:

var accelerometerX: UIAccelerationValue = 0
var accelerometerY: UIAccelerationValue = 0
 
let motionManager = CMMotionManager()

You’ll need these properties to keep track of the Core Motion manager and the accelerometer values. You only need to store the values for the x- and y-axes; the z-axis isn’t used by this game.

Next, add the following utility methods to the class:

func startMonitoringAcceleration() {
 
  if motionManager.accelerometerAvailable {
    motionManager.startAccelerometerUpdates()
    NSLog("accelerometer updates on...")
  }
}
 
func stopMonitoringAcceleration() {
 
  if motionManager.accelerometerAvailable && motionManager.accelerometerActive {
    motionManager.stopAccelerometerUpdates()
    NSLog("accelerometer updates off...")
  }
}

The start and stop methods check to make sure the accelerometer hardware is available on the device and, if so, tell it to start gathering data. The stop method will be called when you wish to turn off acceleration monitoring.

A good place to activate the accelerometers is inside didMoveToView(). Add the following line to it underneath the addChild(playerSprite) line:

startMonitoringAcceleration()

For stopping the accelerometers, a good place is in the class de-initializer. Add the following to the class:

deinit {
  stopMonitoringAcceleration()
}

Next, add the method that will be called to read the values and let your player change positions:

func updatePlayerAccelerationFromMotionManager() {
 
  if let acceleration = motionManager.accelerometerData?.acceleration {
 
    let FilterFactor = 0.75
 
    accelerometerX = acceleration.x * FilterFactor + accelerometerX * (1 - FilterFactor)
    accelerometerY = acceleration.y * FilterFactor + accelerometerY * (1 - FilterFactor)
  }
}

This bit of logic is necessary to filter, or smooth the data that you get back from the accelerometers so that it appears less jittery. The motionManager.accelerometerData property may be nil if no data is yet available, so you use the ?. operator to access the acceleration property, and wrap the logic in if let ... to ensure it will be skipped if there is no acceleration data to work with yet.

Note: An accelerometer records the acceleration currently being applied to it. The iPhone is always under acceleration due to the pull of gravity (which is how iOS knows which way to orient the screen), but because the user is holding the iPhone in their hands (and hands are never completely steady) there are a lot of tiny fluctuations in this gravity value. You’re not so interested in these fluctuations as in the larger, deliberate changes that the user makes to the device orientation. By applying this simple low-pass filter, you retain the orientation information but filter out the less important fluctuations.

Now that you have a stable measurement of the device’s orientation, how can you use this to make the player’s spaceship move?

Motion in physics-based games is typically implemented like this:

  1. First, you set the acceleration, based on some form of user input (in this case the accelerometer values).
  2. Second, you add the new acceleration to the spaceship’s current velocity. This makes the object speed up or slow down, depending on the direction of the acceleration.
  3. Finally, you add the new velocity to the spaceship’s position to make it move.

You have a great mathematician to thank for the equations that control this motion: Sir Isaac Newton!

You’ll need to add some more properties to track the velocity and acceleration. There is no need to keep track of the player’s position because the SKSpriteNode already does that for you.

Note: Technically, Sprite Kit can keep track of velocity and acceleration as well, thanks to the SKPhysicsBody property. Sprite Kit’s physics can track forces on the sprite and update the acceleration, velocity and position automatically. But if you use Sprite Kit’s physics to do all the math, you won’t learn much about trigonometry! So, for this tutorial, you’re going to do all the math yourself.

Add these properties to the class next:

var playerAcceleration = CGVector(dx: 0, dy: 0)
var playerVelocity = CGVector(dx: 0, dy: 0)

It’s good to set some bounds on how fast the spaceship can travel or it would be pretty hard to maneuver. Unlimited acceleration would make the ship tricky to control (not to mention turning the poor pilot into jello!), so let’s set an upper limit.

Add the following lines directly below the import statements:

let MaxPlayerAcceleration: CGFloat = 400
let MaxPlayerSpeed: CGFloat = 200

This defines two constants: The maximum acceleration (400 points per second squared), and the maximum speed (200 points per second). You’ve used the common Swift convention of capitalising the first letter of your configuration constants to distinguish them from regular “let” variables.

Now add the following code to the bottom of the if let ... statement in updatePlayerAccelerationFromMotionManager:

playerAcceleration.dx = CGFloat(accelerometerY) * -MaxPlayerAcceleration
playerAcceleration.dy = CGFloat(accelerometerX) * MaxPlayerAcceleration

Accelerometer values are provided in the range -1 to +1, so to get the final acceleration, you simply multiply the accelerometer value by MaxPlayerAcceleration.

Note: You’re using the accelerometerY value for the x-direction and accelerometerX for the Y-direction. That’s as it should be. Remember that this game is in landscape, so the X-accelerometer runs from top to bottom in this orientation, and the Y-accelerometer from right to left.

You’re almost there. The last step is applying the playerAcceleration.dx and playerAcceleration.dy values to the velocity and position of the spaceship. You will do this from within the game’s update() method. This method is called once per frame (60 times per second), so it’s the natural place to perform all of the game logic.

Add the updatePlayer() method:

func updatePlayer(dt: CFTimeInterval) {
 
  // 1
  playerVelocity.dx = playerVelocity.dx + playerAcceleration.dx * CGFloat(dt)
  playerVelocity.dy = playerVelocity.dy + playerAcceleration.dy * CGFloat(dt)
 
  // 2
  playerVelocity.dx = max(-MaxPlayerSpeed, min(MaxPlayerSpeed, playerVelocity.dx))
  playerVelocity.dy = max(-MaxPlayerSpeed, min(MaxPlayerSpeed, playerVelocity.dy))
 
  // 3
  let newX = playerSprite.position.x + playerVelocity.dx * CGFloat(dt)
  let newY = playerSprite.position.y + playerVelocity.dy * CGFloat(dt)
 
  // 4
  newX = min(size.width, max(0, newX));
  newY = min(size.height, max(0, newY));
 
  playerSprite.position = CGPoint(x: newX, y: newY)
}

If you’ve programmed games before (or studied physics), then this should look familiar. Here’s how it works:

  1. This code adds the current acceleration to the velocity.

    The acceleration is expressed in points per second (actually, per second squared, but don’t worry about that). However, the update() method is executed a lot more often than once per second. To compensate for this difference, you multiply the acceleration by the elapsed or “delta” time, dt. Without this, the spaceship would move about sixty times faster than it should!

  2. This clamps the velocity so that it doesn’t go faster than MaxPlayerSpeed if it is positive or -MaxPlayerSpeed if it is negative.

  3. This adds the current velocity to the sprite’s position. Again, velocity is measured in points per second, so you need to multiply it by the delta time to make it work correctly.
  4. Clamp the new position to the sides of the screen. You don’t want the player’s spaceship to fly off-screen, never to be found again!

One more thing: you need to measure time as differences (deltas) in time. The Sprite Kit update() method gets called repeatedly with the current time, but you’ll need to track the delta time between calls to the update() method ourselves, so that the velocity calculations are smooth.

To track the delta time, add another property:

var lastUpdateTime: CFTimeInterval = 0

Then replace the update() method stub with the actual implementation:

override func update(currentTime: CFTimeInterval) {
 
  // to compute velocities we need delta time to multiply by points per second
  // SpriteKit returns the currentTime, delta is computed as last called time - currentTime
  let deltaTime = max(1.0/30, currentTime - lastUpdateTime)
  lastUpdateTime = currentTime
 
  updatePlayerAccelerationFromMotionManager()
  updatePlayer(deltaTime)
}

That should do it.

You calculate deltaTime by subtracting the last recorded update time from the the current time. Just to be safe, clamp deltaTime to a maximum of 1/30th of a second. That way, if the app’s frame rate should fluctuate or stall for some reason, the ship won’t get catapulted across the screen when the next update occurs.

The updatePlayerAccelerationFromMotionManager() method is called to calculate the players acceleration from the accelerometer values.

Finally, updatePlayer() is called to move the ship, making use of the delta time to compute the velocity.

Build and run the game on an actual device (not the simulator). You can now control the spaceship by tilting the device:

MovingShip

One last thing before you proceed: In GameViewController.swift, find the line:

skView.ignoresSiblingOrder = true

And change it to:

skView.ignoresSiblingOrder = false

This disables a small optimization in the way that sprites are rendered, but it means that sprites will be drawn in the order they are added. This will be useful later.

Begin the Trigonometry!

If you skipped ahead to this section, here is the starter project at this point. Build and run it on your device – you’ll see there’s a spaceship that you can move around with the accelerometer.

You haven’t used any actual trigonometry yet, so let’s put some into action!

It would be cool – and much less confusing to the player – to rotate the spaceship in the direction it is currently moving rather than having it always pointing upward.

To rotate the spaceship, you need to know the angle to rotate it to. But you don’t know what that is; you only have the velocity vector. So how can you get an angle from a vector?

Let’s think about what you do know. The player’s velocity consists of two components: a length in the X-direction and a length in the Y-direction:

VelocityComponents

If you rearrange these a little, you can see that they form a triangle:

VelocityTriangle

Here you know the lengths of the adjacent (playerVelocity.dx) and the opposite (playerVelocity.dy) sides.

So basically, you know 2 sides of a right triangle, and you want to find an angle (the Know 2 Sides, Need Angle case), so you need to use one of the inverse functions: arcsin, arccos or arctan.

The sides you know are the opposite and adjacent sides to the angle you need, so you’ll want to use the arctan function to find the angle to rotate the ship. Remember, that looks like the following:

angle = arctan(opposite / adjacent)

The Swift standard library includes an atan() function that computes the arc tangent, but it has a couple of limitations: x / y yields exactly the same value as -x / -y, which means that you’ll get the same angle output for opposite velocities. Worse than that, the angle inside the triangle isn’t exactly the one you want anyway – you want the angle relative to one particular axis, which may be 90, 180 or 270 degrees offset from the angle returned by atan().

You could write a four-way if-statement to work out the correct angle by taking into account the signs of the velocity components to determine which quadrant the angle is in, and then apply the correct offset. But it turns out there’s a much simpler way:

For this specific problem, instead of using atan(), it is simplier to use the function atan2(), which takes the x and y components as separate parameters, and correctly determines the overall rotation angle.

angle = atan2(opposite, adjacent)

Add the following two lines to the bottom of updatePlayer:

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle

Notice that the Y-coordinate goes first. A common mistake is to write atan(x, y), but that’s the wrong way around. Remember the first parameter is the opposite side, and in this case the Y coordinate lies opposite the angle you’re trying to measure.

Build and run the app to try it out:

ShipPointingWrongWay

Hmm, this doesn’t seem to be working quite right. The spaceship certainly rotates but it’s pointing in a different direction from where it’s flying!

Here’s what’s happening: the sprite image for the spaceship points straight up, which corresponds to the default rotation value of 0 degrees. But by mathematical convention, an angle of 0 degrees doesn’t point upward, but to the right, along the X-axis:

RotationDifferences

To fix this, subtract 90 degrees from the rotation angle so that it matches up with the sprite image:

playerSprite.zRotation = angle - 90

Try it out…

Nope! If anything, it’s even worse now! What’s missing?

Radians, Degrees and Points of Reference

Normal humans tend to think of angles as values between 0 and 360 (degrees). Mathematicians, however, usually measure angles in radians, which are expressed in terms of π (the Greek letter Pi, which sounds like “pie” but doesn’t taste as good).

One radian is defines the angle you get when you travel the distance of the radius along the arc of the circle. You can do that 2π times (roughly 6.28 times) before you end up at the beginning of the circle again.

Notice the yellow line (the radius) is the same length as the red curved line (the arc). That magic angle where the two are equal is one radian!

So while you may think of angles as values from 0 to 360, a mathematician sees values from 0 to 2π. Most computer math functions work in radians, because it’s a more useful unit for doing calculations. Sprite Kit uses radians for all its angular measurements as well. The atan2() function returns a value in radians, but you’ve tried to offset that angle by 90 degrees.

Since you will be working with both radians and degrees, it will be useful if you have a way to easily convert between them. The conversion is pretty simple: Since there are 2π radians or 360 degrees in a circle, π equates to 180 degrees, so to convert from radians to degrees you divide by π and multiply by 180. To convert from degrees to radians you divide by 180 and multiply by π.

The C math library (which is automatically made available to Swift) has a constant, M_PI, that represents the value of π as a double. Swift’s strict casting rules make it inconvenient to use this constant when most of the values you’re dealing with are CGFloat, so you can just define your own constant. In GameScene.swift add the following to the top-level of the file, above the class definition:

let Pi = CGFloat(M_PI)

Now define another two constants that will make it easy to convert between degrees and radians:

let DegreesToRadians = Pi / 180
let RadiansToDegrees = 180 / Pi

Finally, edit the rotation code in updatePlayer again, to include the DegreesToRadians multiplier:

playerSprite.zRotation = angle - 90 * DegreesToRadians

Build and run again and you’ll see that the spaceship is finally rotating correctly.

Bouncing Off the Walls

You have a spaceship that you can move using the accelerometers and you’re using trig to make sure it points in the direction it’s flying. That’s a good start.

Having the spaceship get stuck on the edges of the screen isn’t very satisfying though. You’re going to fix that by making it bounce off the screen borders instead!

First, delete these lines from updatePlayer():

// 4
newX = min(size.width, max(0, newX))
newY = min(size.height, max(0, newY))

And replace them with the following:

var collidedWithVerticalBorder = false
var collidedWithHorizontalBorder = false
 
if newX < 0 {
  newX = 0
  collidedWithVerticalBorder = true
} else if newX > size.width {
  newX = size.width
  collidedWithVerticalBorder = true
}
 
if newY < 0 {
  newY = 0
  collidedWithHorizontalBorder = true
} else if newY > size.height {
  newY = size.height
  collidedWithHorizontalBorder = true
}

This checks whether the spaceship hit any of the screen borders, and if so, sets a Bool variable to true. But what to do after such a collision takes place? To make the spaceship bounce off the border, you can simply reverse its velocity and acceleration.

Add the following lines to updatePlayer(), directly below the code you just added:

if collidedWithVerticalBorder {
  playerAcceleration.dx = -playerAcceleration.dx
  playerVelocity.dx = -playerVelocity.dx
  playerAcceleration.dy = playerAcceleration.dy
  playerVelocity.dy = playerVelocity.dy
}
 
if collidedWithHorizontalBorder {
  playerAcceleration.dx = playerAcceleration.dx
  playerVelocity.dx = playerVelocity.dx
  playerAcceleration.dy = -playerAcceleration.dy
  playerVelocity.dy = -playerVelocity.dy
}

If a collision is registered, you invert the acceleration and velocity values, causing the ship to bounce away again.

Build and run to try it out.

Hmm, the bouncing works, but it seems a bit energetic. The problem is that you wouldn’t expect a spaceship to bounce like a rubber ball – it should lose most of its energy in the collision, and bounce off with less velocity than it had beforehand.

Define another constant at the top of the file, right below the let MaxPlayerSpeed: CGFloat = 200 line:

let BorderCollisionDamping: CGFloat = 0.4

Now, replace the code you just added in updatePlayer with this:

if collidedWithVerticalBorder {
  playerAcceleration.dx = -playerAcceleration.dx * BorderCollisionDamping
  playerVelocity.dx = -playerVelocity.dx * BorderCollisionDamping
  playerAcceleration.dy = playerAcceleration.dy * BorderCollisionDamping
  playerVelocity.dy = playerVelocity.dy * BorderCollisionDamping
}
 
if collidedWithHorizontalBorder {
  playerAcceleration.dx = playerAcceleration.dx * BorderCollisionDamping
  playerVelocity.dx = playerVelocity.dx * BorderCollisionDamping
  playerAcceleration.dy = -playerAcceleration.dy * BorderCollisionDamping
  playerVelocity.dy = -playerVelocity.dy * BorderCollisionDamping
}

You’re now mutliplying the acceleration and velocity by a damping value, BorderCollisionDamping. This allows you to control how much energy is lost in the collision. In this case, you make the spaceship retain only 40% of its speed after bumping into the screen edges.

For fun, play with the value of BorderCollisionDamping to see the effect of different values for this constant. If you make it larger than 1.0, the spaceship actually gains energy from the collision!

You may have noticed a slight problem: Keep the spaceship aimed at the bottom of the screen so that it continues smashing into the border over and over, and you’ll see that it starts to stutter between pointing up and pointing down.

Using the arc tangent to find the angle between a pair of X and Y components works quite well, but only if those X and Y values are fairly large. In this case, the damping factor has reduced the speed to almost zero. When you apply atan2() to very small values, even a tiny change in these values can result in a big change in the resulting angle.

One way to fix this is to not change the angle when the speed is very slow. That sounds like an excellent reason to give a call to your old friend, Pythagoras.

pythagoras

Right now you don’t actually store the ship’s speed. Instead, you store the velocity, which is the vector equivalent (see here for an explanation of the difference between speed and velocity), with one component in the X-direction and one in the Y-direction. But in order to draw any conclusions about the ship’s speed (such as whether it’s too slow to be worth rotating the ship) you need to combine these X and Y speed components into a single scalar value.

Pythagoras

Here you are in the Know 2 Sides, Need Remaining Side case, discussed earlier.

As you can see, the true speed of the spaceship – how many points it moves across the screen per second – is the hypotenuse of the triangle that is formed by the speed in the X-direction and the speed in the Y-direction.

Put in terms of the Pythagorean formula:

true speed = √(playerVelocity.dx2 + playerVelocity.dy2)

Remove this block of code from updatePlayer():

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle - 90 * DegreesToRadians

And replace it with this:

let RotationThreshold: CGFloat = 40
 
let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > RotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
  playerSprite.zRotation = angle - 90 * DegreesToRadians
}

Build and run. You’ll see the spaceship rotation seems a lot more stable at the edges of the screen. If you’re wondering where the value 40 came from, the answer is: experimentation. Putting NSLog() statements into the code to look at the speeds at which the craft typically hit the borders helped in tweaking this value until it felt right :]

Blending Angles for Smooth Rotation

Of course, fixing one thing always breaks something else. Try slowing down the spaceship until it has stopped, then flip the device so the spaceship has to turn around and fly the other way.

Previously, that would happen with a nice animation where you actually saw the ship turning. But because you just added some code that prevents the ship from changing its angle at low speeds, the turn is now very abrupt. It’s only a small detail, but it’s the details that make great apps and games.

The fix is to not switch to the new angle immediately, but to gradually blend it with the previous angle over a series of successive frames. This re-introduces the turning animation and still prevents the ship from rotating when it is not moving fast enough.

This “blending” sounds fancy, but it’s actually quite easy to implement. It will require you to keep track of the spaceship’s angle between updates, however, so add a new property for it in the implementation of the GameScene class:

var playerAngle: CGFloat = 0

Update the rotation code in updatePlayer() to this:

let RotationThreshold: CGFloat = 40
let RotationBlendFactor: CGFloat = 0.2
 
let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > RotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
  playerAngle = angle * RotationBlendFactor + playerAngle * (1 - RotationBlendFactor)
  playerSprite.zRotation = playerAngle - 90 * DegreesToRadians
}

The playerAngle variable combines the new angle and its own previous value by multiplying them with a blend factor. In human-speak, this means the new angle only contributes 20% towards the actual rotation that you set on the spaceship. Over time, more and more of the new angle gets added so that eventually the spaceship points in the correct direction.

Build and run to verify that there is no longer an abrupt change from one rotation angle to another.

Now try flying in a circle a couple of times, both clockwise and counterclockwise. You’ll notice that at some point in the turn, the spaceship suddenly spins 360 degrees in the opposite direction. It always happens at the same point in the circle. What’s going on?

The atan2() returns and angle between +π and –π (between +180 and -180 degrees). That means that if the current angle is very close +π, and then it turns a little further, it’s going to wrap around to -π (or vice-versa).

That’s actually equivalent to the same position on the circle (just like -180 and +180 degrees are the same point), but your blending algorithm isn’t smart enough to realise that – it thinks the angle has jumped a whole 360 degrees (aka 2π radians) in one step, and it needs to spin the ship 360 degrees in the opposite direction to catch back up.

To fix it, you need to recognize when the angle crosses that threshold, and adjust playerAngle accordingly. Add a new property to the GameScene class:

var previousAngle: CGFloat = 0

And change the rotation code one more time to this:

let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > RotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
 
  // did angle flip from +π to -π, or -π to +π?
  if angle - previousAngle > Pi {
    playerAngle += 2 * Pi
  } else if previousAngle - angle > Pi {
    playerAngle -= 2 * Pi
  }
 
  previousAngle = angle
  playerAngle = angle * RotationBlendFactor + playerAngle * (1 - RotationBlendFactor)
  playerSprite.zRotation = playerAngle - 90 * DegreesToRadians
}

Now you’re checking the difference between the current angle and the previous angle to watch for changes over the thresholds of 0 and π (180 degrees).

Build and run. That’ll fix things right up and you should have no more problems turning your spacecraft!

Using Trig to Find Your Target

This is a great start – you have a spaceship moving along pretty smoothly! But so far the little spaceship’s life is too easy and carefree. Let’s spice things up by adding an enemy: a big cannon!

Add two new properties to the GameScene class:

let cannonSprite = SKSpriteNode(imageNamed: "Cannon")
let turretSprite = SKSpriteNode(imageNamed: "Turret")

You’ll set these sprites up in didMoveToView(). Place this code before the setup for playerSprite, so that the spaceship always gets drawn after (and therefore in front of) the cannon:

cannonSprite.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(cannonSprite)
 
turretSprite.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(turretSprite)

Note: Remember that change you made to set skView.ignoresSiblingOrder = false earlier? That ensures that sprites are drawn in the order they are added to their parent. There are other ways to control sprite drawing order – such as using the zPosition – but this is the simplest.

The cannon consists of two sprites: the fixed base, and the turret that can rotate to take aim at the player. Build and run, and you should see a brand-new cannon sitting smack in the middle of the screen.

Cannon

Now to give the cannon a target to snipe at!

You want the cannon’s turret to point at the player at all times. To get this to work, you’ll need to figure out the angle between the turret and the player.

Figuring this out will be very similar to how you calculated how to rotate the spaceship to face the direction it’s moving in. The difference is that this time, the triangle won’t be derived from the velocity of the spaceship; instead, it will be drawn between the centers of the two sprites:

Again, you can use atan2() to calculate this angle. Add the following method:

func updateTurret(dt: CFTimeInterval) {
 
  let deltaX = playerSprite.position.x - turretSprite.position.x
  let deltaY = playerSprite.position.y - turretSprite.position.y
  let angle = atan2(deltaY, deltaX)
 
  turretSprite.zRotation = angle - 90 * DegreesToRadians
}

The deltaX and deltaY variables measure the distance between the player sprite and the turret sprite. You plug these values into atan2() to get the relative angle between them.

As before, you need to convert this angle to include the offset from the X-axis (90 degrees) so the sprite is oriented correctly. Remember that atan2() always gives you the angle between the hypotenuse and the 0-degree line; it’s not the angle inside the triangle.

Finally, add a call this new method. Find update() and add the following code to the end of that method:

updateTurret(deltaTime)

Build and run. The turret will now always point toward the spaceship. See how easy that was? That’s the power of trig for you!

TurretTrackingPlayer

Challenge: It is unlikely that a real cannon would be able to move instantaneously – it would have to be able to predict exactly where the target was going. Instead, it would always be playing catch up, trailing the position of the ship slightly.

You can accomplish this by “blending” the old angle with the new one, just like you did earlier with the spaceship’s rotation angle. The smaller the blend factor, the more time the turret needs to catch up with the spaceship. See if you can implement this on your own.

Adding Health Bars

In part 2, you’ll add code to let player fire missiles at the cannon, and the cannon will be able to inflict damage on the player. To show the amount of hit points each object has remaining, you will need to add some health bar sprites to the scene. Let’s do that now.

Add the following new constants to the top of the GameScene.swift file:

let MaxHealth = 100
let HealthBarWidth: CGFloat = 40
let HealthBarHeight: CGFloat = 4

Also, add these new properties to the GameScene class:

let playerHealthBar = SKSpriteNode()
let cannonHealthBar = SKSpriteNode()
 
var playerHP = MaxHealth
var cannonHP = MaxHealth

Now, insert the following code into didMoveToView(), just before startMonitoringAcceleration():

addChild(playerHealthBar)
 
addChild(cannonHealthBar)
 
cannonHealthBar.position = CGPoint(
  x: cannonSprite.position.x,
  y: cannonSprite.position.y - cannonSprite.size.height/2 - 10
)

The playerHealthBar and cannonHealthBar objects are SKSpriteNodes, but you haven’t specified an image to display for them. Instead, you will be drawing the health bar images dynamically using Core Graphics.

Note that you placed the cannonHealthBar sprite slightly below the cannon, but didn’t assign a position to the playerHealthBar yet. That’s because the cannon never moves, so you can simply set the position of its health bar once and forget about it.

Whenever the spaceship moves though, you’ll have to adjust the position of the playerHealthBar as well. That happens in updatePlayer. Add these lines to the bottom of that method:

playerHealthBar.position = CGPoint(
  x: playerSprite.position.x,
  y: playerSprite.position.y - playerSprite.size.height/2 - 15
)

Now all that’s left is to draw the bars themselves. Add this new method to the class:

func updateHealthBar(node: SKSpriteNode, withHealthPoints hp: Int) {
 
  let barSize = CGSize(width: HealthBarWidth, height: HealthBarHeight);
 
  let fillColor = UIColor(red: 113.0/255, green: 202.0/255, blue: 53.0/255, alpha:1)
  let borderColor = UIColor(red: 35.0/255, green: 28.0/255, blue: 40.0/255, alpha:1)
 
  // create drawing context
  UIGraphicsBeginImageContextWithOptions(barSize, false, 0)
  let context = UIGraphicsGetCurrentContext()
 
  // draw the outline for the health bar
  borderColor.setStroke()
  let borderRect = CGRect(origin: CGPointZero, size: barSize)
  CGContextStrokeRectWithWidth(context, borderRect, 1)
 
  // draw the health bar with a colored rectangle
  fillColor.setFill()
  let barWidth = (barSize.width - 1) * CGFloat(hp) / CGFloat(MaxHealth)
  let barRect = CGRect(x: 0.5, y: 0.5, width: barWidth, height: barSize.height - 1)
  CGContextFillRect(context, barRect)
 
  // extract image
  let spriteImage = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()
 
  // set sprite texture and size
  node.texture = SKTexture(image: spriteImage)
  node.size = barSize
}

This code draws a single health bar. First it sets up the fill and border colors, then it creates a drawing context, and draws two rectangles: the border, which always has the same size, and the bar itself, which varies in width depending on the number of hit points. The method then generates a UIImage from the drawing context, and assigns it as the texture for the sprite.

You need to call this method twice, once for the player and once for the cannon. Because redrawing the health bar is relatively expensive (Core Graphics drawing isn’t hardware accelerated), you don’t want to do it every frame. Instead, you’ll call this code only when the player’s or cannon’s health changes. For now, you’ll call it just once to set the initial appearance for the bars.

Add the following code to the end of didMoveToView:

startMonitoringAcceleration()

Build and run. Now, both the player and the cannon have health bars:

HealthBars

Using Trig for Collision Detection

Right now, the spaceship can fly directly through the cannon without consequence. It would be more challenging (and realistic) if it suffered damage when colliding with the cannon. This is where you enter the sphere of collision detection (sorry about the pun! :])

At this point, a lot of game devs would think, “I need a physics engine!” and while it’s certainly true that you can use Sprite Kit’s physics for this, it’s not that hard to do collision detection yourself, especially if you model the sprites using simple circles.

Detecting whether two circles intersect is a piece of cake: all you have to do is calculate the distance between them (*cough* Pythagoras) and see if it is smaller than the sum of the radii (or “radiuses” if you prefer) of both circles.

Add two new constants to the top of GameScene.swift:

let CannonCollisionRadius: CGFloat = 20
let PlayerCollisionRadius: CGFloat = 10

These are the sizes of the collision circles around the cannon and the player. Looking at the sprite, you’ll see that the actual radius of the cannon image in pixels is slightly larger than the constant you’ve specified (around 25 points), but it’s nice to have a bit of wiggle room; you don’t want your games to be too unforgiving, or players won’t have fun.

The fact that the spaceship isn’t circular at all shouldn’t deter you. A circle is often a good enough approximation for the shape of an arbitrary sprite, and it has the big advantage that it makes it much simpler to do the trig calculations. In this case, the body of the ship is roughly 20 points in diameter (remember, the diameter is twice the radius).

Add a new method to the class to do the collision detection:

func checkShipCannonCollision() {
 
  let deltaX = playerSprite.position.x - turretSprite.position.x
  let deltaY = playerSprite.position.y - turretSprite.position.y
 
  let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
  if distance <= CannonCollisionRadius + PlayerCollisionRadius {
    runAction(collisionSound)
  }
}

You’ve seen how this works before: first you calculate the distance between the X-positions of the two sprites, then the Y-positions. Treating these two values as the sides of a right triangle, you can then calculate the hypotenuse, which is the true distance between these sprites.

If that distance is smaller than the sum of the collision radii, play a sound effect. You’ll see an error on that line will error for now, because you haven’t added the sound effect code yet – it’s coming soon, so just be patient!

Add a call to this new method at the end of update():

checkShipCannonCollision()

Then, add this property to the top of the GameScene class:

let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false)

Time to build and run again. Give the collision logic a whirl by flying the spaceship into the cannon.

Overlap

Notice that the sound effect plays endlessly as soon as a collision begins. That’s because, while the spaceship flies over the cannon, the game registers repeated collisions, one after another. There isn’t just one collision, there are 60 per second, and it plays the sound effect for every one of them!

Collision detection is only the first half of the problem. The second half is collision response. Not only do you want audio feedback from the collision, but you also want a physical response – the spaceship should bounce off the cannon.

Add this constant to the top of the GameScene.swift file:

let CollisionDamping: CGFloat = 0.8

Then add these lines inside the if statement in checkShipCannonCollision():

playerAcceleration.dx = -playerAcceleration.dx * CollisionDamping
playerAcceleration.dy = -playerAcceleration.dy * CollisionDamping
playerVelocity.dx = -playerVelocity.dx * CollisionDamping
playerVelocity.dy = -playerVelocity.dy * CollisionDamping

This is very similar to what you did to make the spaceship bounce off the screen borders. Build and run to see how it works.

It looks pretty good if the spaceship is going fast when it hits the cannon. But if it’s moving too slowly, then even after reversing the speed, the ship sometimes stays within the collision radius and never makes its way out of it. Clearly, this solution has some problems.

Instead of just bouncing the ship off the cannon by reversing its velocity, you need to physically push the ship away from the cannon by adjusting its position so that the radii no longer overlap.

To do this, you’ll need to calculate the vector between the cannon and the spaceship, which, fortunately, you already calculated earlier in order to measure the distance between them. So how do you use that distance vector to move the ship?

The vector formed by deltaX and deltaY is already pointing in the right direction, but it’s the wrong length. The length you need it to be is the difference between the radii of the ships and its current length – that way, when you add it to the ship’s current position, the ship will no longer be overlapping the cannon.

The current length of the vector is distance, but the length that you need it to be is:

CannonCollisionRadius + PlayerCollisionRadius – distance

So how can you change the length of a vector?

The solution is to use a technique called “normalization”. You normalize a vector by dividing the X and Y components by the current scalar length (calculated using Pythagoras). The resultant “normal” vector, has an overall length of one.

Then, you just multiply the X and Y by the desired length to get the offset for the spaceship. Add the following code immediately underneath the previous lines you added:

let offsetDistance = CannonCollisionRadius + PlayerCollisionRadius - distance
let offsetX = deltaX / distance * offsetDistance
let offsetY = deltaY / distance * offsetDistance
playerSprite.position = CGPoint(
  x: playerSprite.position.x + offsetX,
  y: playerSprite.position.y + offsetY
)

Build and run, and you’ll see the spaceship now bounces properly off the cannon.

To round off the collision logic, you’ll subtract some hit points from the spaceship and the cannon, and update the health bars. Add the following code inside the if statement:

playerHP = max(0, playerHP - 20)
cannonHP = max(0, cannonHP - 5)
 
updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)

Build and run again. The ship and cannon now lose a few hit points each time they collide.

Damage

Adding Some Spin

For a nice effect, you can add some spin to the spaceship after a collision. This is additional rotation that doesn’t influence the flight direction; it just makes the effect of the collision more profound (and the pilot more dizzy). Add a new constant to the top of GameScene.swift:

let PlayerCollisionSpin: CGFloat = 180

This sets the amount of spin to half a circle per second, which I think looks pretty good. Now add a new property to the GameScene class:

var playerSpin: CGFloat = 0

In checkShipCannonCollision(), add the following line inside the if statement:

playerSpin = PlayerCollisionSpin

Finally, add the following code to updatePlayer(), immediately before the line playerSprite.zRotation = playerAngle - 90 * DegreesToRadians:

if playerSpin > 0 {
 
  playerAngle += playerSpin * DegreesToRadians
  previousAngle = playerAngle
  playerSpin -= PlayerCollisionSpin * CGFloat(dt)
  if playerSpin < 0 {
    playerSpin = 0
  }
}

The playerSpin effectively just overrides the display angle of the ship for the duration of the spin, without affecting the velocity. The amount of spin quickly decreases over time, so that the ship comes out of the spin after one second. While spinning, you update previousAngle to match the spin angle, so that the ship doesn’t suddenly snap to a new angle after coming out of the spin.

Build and run and set that ship spinning!

Where to Go from Here?

Here is the full example project from the tutorial up to this point.

Triangles are everywhere! You’ve seen how you can use this fact to breathe life into your sprites with the various trigonometric functions to handle movement, rotation and even collision detection.

You have to admit, it wasn’t that hard to follow along, was it? Math doesn’t have to be boring if you can apply it to fun projects, such as making games!

But there’s more to come: in Part 2 of this Trigonometry for Game Programming series, you’ll add missiles to the game, learn more about sine and cosine, and see some other useful ways to put the power of trig to work in your games.

Credits: The graphics for this game are based on a free sprite set by Kenney Vleugels. The sound effects are based on samples from freesound.org.

Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 1/2 is a post from: Ray Wenderlich

The post Trigonometry for Games – Sprite Kit and Swift Tutorial: Part 1/2 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>