Learn Trigonometry for game programming!
Note from Ray: Tony has ported this popular tutorial from Cocos2D to Sprite Kit. We hope you enjoy!
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 back up that claim!
Here’s a little secret: as an app developer, you don’t really need to know a lot of math. If you can add or multiply two numbers together, you’re already halfway there. 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 Einstein, 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 spaces shooter iPhone game that requires a lot of trigonometry, 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 we’ll cover in this tutorial will apply to your engine no matter what. 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!
Getting Started: It’s All About Triangles
It sounds like a mouthful, but trigonometry simply means calculations with triangles (that’s where the tri comes from).
You may not have realized it until now, but games are full of triangles. For example, imagine you have a spaceship game, and you want to calculate the distance between these 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 some Trigonometry 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 also known as a right triangle, 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 of the sides of these triangles, as well as the angles between those sides. And that comes in handy more often that you might think.
For example, in this space ship 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 other corner (top-right), then the adjacent and opposite change 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, trig allows you to find out all the others using the sine, cosine and tangent functions. For example, if you know an angle and the length of one of the sides, then the sine, cosine and tangent functions can tell you the length of the other sides:
Think of the sin, cos, and tan functions as “black boxes” – you plug in numbers and get back results. They are pre-written functions you can call from almost any programming language.
Know Angle and Length, Need Sides
Let’s consider an example. Say you know the alpha angle between the ships is 45 degrees, and the length between the ships (the hypotenuse) is 10 points long. You can then plug these values into the formula:
sin(45) = opposite / 10
To solve this for the hypotenuse, you shift things around a bit:
opposite = sin(45) * 10
If you call the built-in sin function, you’ll find the sine of 45 degrees is 0.707 (rounded off), and filling that in that 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 above formulas are useful when you already know an angle, but that is not always the case – sometimes it is the angle you are looking for. Then you need to know the lengths of at least two of the sides and plug these into the inverse functions:
- angle = arcsin(opposite/hypotenuse)
- angle = arccos(adjacent/hypotenuse)
- angle = arctan(opposite/adjacent)
In other words, if sin(a) = b, then it is also true that arcsin(b) = a. Of these inverse functions, you will use the arc tangent (arctan) the most in practice. Sometimes these functions are also notated as sin-1, cos-1, and tan-1, so don’t let that fool you.
Is any of this sinking in or sounding familiar? Good, but 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 need to know the length of the third, like in the example at the beginning of this tutorial where you wanted to find the distance between the two space ships.
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 of these sides, then 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 also know beta, and vice-versa.
And those are all the formulas 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 space ship 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:
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 5 or later, since it is the first version of Xcode to include Sprite Kit.
Next start up Xcode, select File\New\Project…, choose iOS\Application\SpriteKit Game template, and click Next:
Name the project TrigBlasterSK and set the device family to iPhone. Build and run the template as-is. If all works OK, you should see the following:
Download the resources for this tutorial. This file contains the images for the sprites and the sound effects. Unzip it and drag the art folder into Xcode to add it to the project. In the import dialog, make sure Copy items into destination group’s folder is checked.
Great, the preliminaries are over and done with – now let’s get coding for real.
Steering with Accelerometers
Because this is a simple game, you will be doing all your work inside a single class: the MyScene.m
. Right now, this scene contains a bunch of stuff that you don’t need. For starters, it does not start up with the correct orientation so let’s fix that first.
Switching to Landscape Orientation
First, open your target setting by clicking your TrigBlasterSK project in the Project Navigator, selecting the TrigBlasterSK target. Then, in the Deployment Info section make sure General is checked at the top, under Device Oritentation uncheck all but Landscape Right as shown below:
If you build and run, the app will launch in Landscape orientation but its graphic context is still not landscape. The details of this are covered in our space game tutorial, but for now you need to have the ViewController.m
file changed a bit so the start up code is executed later in the process.
Select the ViewController.m
and replace the viewDidLoad
method with the following:
- (void)viewDidLoad
{
[super viewDidLoad];
}
-(void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
// Configure the view.
// Configure the view after it has been sized for the correct orientation.
[self startScene];
}
- (void)startScene
{
SKView *skView = (SKView *)self.view;
if (!skView.scene) {
skView.showsFPS = YES;
skView.showsNodeCount = YES;
// Create and configure the scene.
MyScene *theScene = [MyScene sceneWithSize:skView.bounds.size];
theScene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:theScene];
}
}
- (BOOL)prefersStatusBarHidden
{
return YES;
} |
The above code moved the scene creation to the viewWillLayoutSubviews
method where the layout orientation is known. There is also an added method at the bottom prefersStatusBarHidden
that will cause the status bar to be removed from your game while it is running-giving more realism during play.
Replace the contents of MyScene.m
with:
#import "MyScene.h"
@implementation MyScene
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithRed:94.0/255.0 green:63.0/255.0 blue:107.0/255.0 alpha:1.0];
}
return self;
}
-(void)update:(NSTimeInterval)currentTime {
/* Called before each frame is rendered */
}
@end |
Build and run, and you should see nothing but purple:
Let’s make things a bit more exciting by adding a spaceship to the scene. You will need some instance variables for this, so modify the @implementation
line in MyScene.m
as follows:
@implementation MyScene
{
CGSize _winSize;
SKSpriteNode *_playerSprite;
} |
The _winSize
variable stores the dimensions of the screen, which is useful because you will be referring to that quite often. The _playerSprite
variable holds the spaceship sprite.
Now change initWithSize
to the following:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithRed:94.0/255.0 green:63.0/255.0 blue:107.0/255.0 alpha:1.0];
_winSize = CGSizeMake(size.width, size.height);
_playerSprite = [SKSpriteNode spriteNodeWithImageNamed:@"Player"];
_playerSprite.position = CGPointMake(_winSize.width - 50.0f, 60.0f);
[self addChild:_playerSprite];
}
return self;
} |
This is all pretty basic if you have worked with Sprite Kit before. The player sprite is positioned in the bottom-right corner of the screen. Remember that with Sprite Kit, it is the bottom of the screen that has y-coordinate 0, unlike in UIKit, where y = 0 points to the top of the screen. You have moved the x-coordinate to 60.0 to get it above the status text being shown.
Build and run to try it out, and you should see the following:
To move the spaceship, this game uses the iPhone’s built-in accelerometers. Unfortunately, the iOS Simulator cannot simulate the accelerometers. That means that from now on, you will need to run the app on your device to test it.
Note: If you are unsure how to put the app on your device, check out this extensive tutorial that explains how to obtain and install the certificates and provisioning profiles that allow Xcode to run apps on your device. It’s not as intimidating as it looks, but you do need to sign up for the paid Apple developer program.
To move the spaceship with the accelerometers, you’ll obviously be rocking your device from side to side. During testing I found that this sometimes caused the screen to autorotate from landscape right to landscape left, which is really annoying when you’re in the middle of a heated battle.
This was the reason you used the Project Settings\General screen and in the Device Orientation, de-selected all options except for Landscape Right earlier.
Using the accelerometers from code is pretty straightforward through the new CoreMotion framework. There are two ways to get accelerometer data: pushed to your application at a specific frequency, and where you request 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) since it can drain the batteries faster.
Your game has a natural place to read the accelerometer data, that’s in the update
method that gets called by Sprite Kit for every loop. You will grab the these values to change the speed of the spaceship in this method.
You need to do some setup to use core motion but it is pretty easy.
First declare your intent to use the CoreMotion framework and bring it in through the new @import syntax. On the line before your #import "MyScene.h"
add the following line:
This new syntax tells Xcode you wish to use the CoreMotion framework and also to expose the .h headers for you. Pretty neat, compared to the old way of accessing external frameworks.
Next you need some instance variables to keep track of the accelerometer values. You only store the values for two of the accelerometers; the “Z” one isn’t used by this game. Add the following lines to your implementation area:
@implementation MyScene
{
. . .
UIAccelerationValue _accelerometerX;
UIAccelerationValue _accelerometerY;
CMMotionManager *_motionManager;
} |
Now create a couple of utility methods for using the accelerometers and to cleanup should we exit the game. Underneath the initWithSize
method add the following methods:
- (void)dealloc
{
[self stopMonitoringAcceleration];
_motionManager = nil;
}
- (void)startMonitoringAcceleration
{
if (_motionManager.accelerometerAvailable) {
[_motionManager startAccelerometerUpdates];
NSLog(@"accelerometer updates on...");
}
}
- (void)stopMonitoringAcceleration
{
if (_motionManager.accelerometerAvailable && _motionManager.accelerometerActive) {
[_motionManager stopAccelerometerUpdates];
NSLog(@"accelerometer updates off...");
}
} |
The start and stop methods check to make sure the capability is present on the device and if so tell it to start gathering data. The stop method will be called when you wish to turn off the data gathering.
A good place to activate the accelerometers is inside initWithSize
. Add the following line to it underneath the [self addChild:_playerSprite];
line:
_motionManager = [[CMMotionManager alloc] init];
[self startMonitoringAcceleration]; |
Next, add the method that will be called to read the values and let your player change positions:
- (void)updatePlayerAccelerationFromMotionManager
{
const double FilteringFactor = 0.75;
CMAcceleration acceleration = _motionManager.accelerometerData.acceleration;
_accelerometerX = acceleration.x * FilteringFactor + _accelerometerX * (1.0 - FilteringFactor);
_accelerometerY = acceleration.y * FilteringFactor + _accelerometerY * (1.0 - FilteringFactor);
} |
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.
Note: An accelerometer records how much gravity currently pulls on it. Because the user is holding the iPhone in her hands, and hands are never completely steady, there are a lot of tiny fluctuations in this gravity value. We are not so much interested in these unsteady motions as in the larger changes in orientation that the user makes to the device. By applying this simple low-pass filter, you retain this 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?
Movement in games often works like this:
- Set the acceleration based on some form of user input, in this case from the accelerometer values.
- Add the new acceleration to the spaceship’s current speed. This makes the object speed up or slow down, depending on the direction of the acceleration.
- Add the new speed to the spaceship’s position to make it move.
Of course, you have a great mathematician to thank for these equations: Sir Isaac Newton!
You need some more instance variables to pull this off. There is no need to keep track of the player’s position because the SKSpriteNode
already does that for you, but acceleration and speed are your responsibility.
Add these instance variables:
@implementation MyScene
{
. . .
float _playerAccelX;
float _playerAccelY;
float _playerSpeedX;
float _playerSpeedY;
} |
It’s good to set some bounds on how fast the spaceship can travel or it would be pretty hard to maneuver. Infinite acceleration sounds like a good idea in theory but in practice it doesn’t work out so well (besides, even Einstein thinks there are limits to how fast you can go).
Add the following lines directly below the #import
statement:
const float MaxPlayerAccel = 400.0f;
const float MaxPlayerSpeed = 200.0f; |
This defines two constants, the maximum acceleration (400 points per second squared) and the maximum speed (200 points per second).
Now add the following bit of logic to the bottom of updatePlayerAccelerationFromMotionManager
:
if (_accelerometerY > 0.05)
{
_playerAccelX = -MaxPlayerAccel;
}
else if (_accelerometerY < -0.05)
{
_playerAccelX = MaxPlayerAccel;
}
if (_accelerometerX < -0.05)
{
_playerAccelY = -MaxPlayerAccel;
}
else if (_accelerometerX > 0.05)
{
_playerAccelY = MaxPlayerAccel;
} |
This is a basic technique for controlling a sprite using the accelerometers. When the device is tilted to the left, you give the player maximum acceleration to the left. Conversely, when the device is tilted to the right, you give the player maximum acceleration to the right. Likewise for the up and down directions.
Note: You’re using the _accelerometerY
value for the x-direction. That’s as it should be. Remember that this game is in landscape, so the Y-accelerometer runs from left to right in this orientation, and the X-accelerometer from top to bottom.
You’re almost there. The last step is applying the _playerAccelX
and Y
values to the speed and position of the spaceship. You will do this in the game’s update
method. This method is called 60 times per second, so it’s the natural place to perform all of the game logic.
Add updatePlayer
to the class:
- (void)updatePlayer:(NSTimeInterval)dt
{
// 1
_playerSpeedX += _playerAccelX * dt;
_playerSpeedY += _playerAccelY * dt;
// 2
_playerSpeedX = fmaxf(fminf(_playerSpeedX, MaxPlayerSpeed), -MaxPlayerSpeed);
_playerSpeedY = fmaxf(fminf(_playerSpeedY, MaxPlayerSpeed), -MaxPlayerSpeed);
// 3
float newX = _playerSprite.position.x + _playerSpeedX*dt;
float newY = _playerSprite.position.y + _playerSpeedY*dt;
// 4
newX = MIN(_winSize.width, MAX(newX, 0));
newY = MIN(_winSize.height, MAX(newY, 0));
_playerSprite.position = CGPointMake(newX, newY);
} |
If you’ve programmed games before (or studied physics), then this should look very familiar. Here’s how it works:
-
Add the current acceleration to the speed.
The acceleration is expressed in points per second (actually, per second squared, but don’t worry about that) but update:
is performed 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!
-
Clamp the speed so that it doesn’t go faster than MaxPlayerSpeed
if it is positive or -MaxPlayerSpeed
if it is negative. You could write this using an if
statement instead:
if (_playerSpeedX < -MaxPlayerSpeed)
{
_playerSpeedX = -MaxPlayerSpeed;
}
else if (_playerSpeedX > MaxPlayerSpeed)
{
_playerSpeedX = MaxPlayerSpeed;
} |
However, I like the succinctness of the one-line version. fminf()
makes sure the speed doesn’t become larger than MaxPlayerSpeed
because it always picks the smallest of the two, while fmaxf()
makes sure the speed doesn’t drop below -MaxPlayerSpeed
because it always picks the largest value. It’s a neat little trick.
- Add the current speed to the sprite’s position. Again, speed is measured in points per second, so you need to multiply it by the delta time to make it work correctly.
- Clamp the new position to the sides of the screen. You don’t want the player’s spaceship to go offscreen, never to be found again!
One more thing, we need to measure time as differences (deltas) in time. The Sprite Kit update
method gets called repeatedly with the current time. We will need to track the delta time between calls to the update method ourselves, so that your velocity calculations are smooth.
To track “delta” time add two more instance variables:
@implementation MyScene
{
. . .
NSTimeInterval _lastUpdateTime;
NSTimeInterval _deltaTime;
} |
Then replace the update
stub with the actual implementation:
-(void)update:(NSTimeInterval)currentTime {
/* Called before each frame is rendered */
//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
if (_lastUpdateTime) {
_deltaTime = currentTime - _lastUpdateTime;
} else {
_deltaTime = 0;
}
_lastUpdateTime = currentTime;
[self updatePlayerAccelerationFromMotionManager];
[self updatePlayer:_deltaTime];
} |
That should do it.
The updatePlayerAccelerationFromMotionManager
is called to calculate the players acceleration from the accelerometer values and save/clamp them.
Finally, the updatePlayer
method 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:
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 seen any 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. But you don’t know what that is, so how can you figure that out?
Let’s think about what you do know. You do have the player’s speed, which consists of two components: a speed in the x-direction and a speed in the y-direction:
If you rearrange these a little, you can see that they form a triangle:
Here you know the lengths of the adjacent (_playerSpeedX
) and the opposite (_playerSpeedY
) sides.
So basically, you know 2 sides of a right triangle, so you are in the Know 2 Sides, Need Angle case mentioned in the beginning of this tutorial. That means you need to use one of the inverse functions: arcsin, arccos or arctan.
You know the opposite and adjacent sides, so you 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 Objective-C math library comes with the atan()
function that computes the arc tangent, but it has one big issue: what if the x-speed is 0? In that case, the adjacent is 0 and dividing by 0 is mathematically undefined. Your app might crash or just behave weirdly when that happens.
Instead of using atan()
, it is better to use the function atan2()
, which takes two parameters and correctly handles the division-by-zero scenario without you having to worry about it:
angle = atan2(opposite, adjacent)
Important: The angle that atan2f()
gives you is not the inner angle inside the triangle, but the angle that the hypotenuse makes with that 0-degree line:
This is another reason why atan2()
is a lot more useful than plain, old atan()
.
So let’s give that a shot. Add the following two lines to the bottom of updatePlayer
:
float angle = atan2f(_playerSpeedY, _playerSpeedX);
_playerSprite.zRotation = angle; |
Note that the arctan function you are using is actually called atan2f()
instead of just atan2()
. More about that in a moment. Also notice that the y-coordinate goes first. A common mistake is to write atan2f(x, y)
but that’s the wrong way around. Remember the first parameter is the opposite, and in this case the Y coordinate is the opposite.
Build and run the app to try it out:
Hmm, this doesn’t seem to be working quite right. What is wrong here?
Radians, Degrees and Points of Reference
Normal human beings tend to think of angles as values between 0 and 360. Mathematicians, however, measure angles in radians, which are expressed in terms of π (the Greek letter Pi, which sounds like pie but doesn’t taste as good).
It’s not essential to understand, but if you’re curious, one radian is 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.
So while you may think of angles as values from 0 to 360, a mathematician sees values from 0 to 2π. Luckily for us Sprite Kit uses radians for its angles as well. The atan2f()
function returns a value in radians but the direction we are pointing is off by 90 degrees.
Since you will be working with both radians and degrees, put these helper macros in that make this easy. At the top of your MyScene.m
file, underneath the #import "MyScene.h"
add the following two defines:
#define SK_DEGREES_TO_RADIANS(__ANGLE__) ((__ANGLE__) * 0.01745329252f) // PI / 180
#define SK_RADIANS_TO_DEGREES(__ANGLE__) ((__ANGLE__) * 57.29577951f) // PI * 180 |
I don’t have these formulas memorized but they are easy enough to derive yourself when you realize than an angle of 360 degrees corresponds to 2π:
Angle in degrees / 360 degrees = Angle in radians / 2π radians
To go from radians to degrees, the formula becomes:
Angle in degrees = (Angle in radians / 2π) * 360
And the other way around, from degrees to radians:
Angle in radians = (Angle in degrees / 360) * 2π
I point this out because forgetting to convert between radians and degrees is probably the most common mistake programmers make when they are dealing with trigonometry! So if you don’t see the rotation you expected, make sure you’re not mixing up your degrees and radians…
So what the @#! is π? Pi is the ratio of the circumference of a circle to its diameter. In other words, if you measure the outside of any circle and divide it by its diameter (which is twice the radius), you get the number 3.141592… and that is what we call π. It takes 2π radians to go all the way around a circle, which is why 360 degrees corresponds to 2π. Pi is a very important concept in mathematics that shows up all the time, especially with anything that is cyclical.
By the way, not everyone likes π. Some people believe that it would have been better to choose the ratio of the circumference to the radius, instead of to the diameter. This number is called tau (τ) and is equal to 2π, thus simplifying many of the calculations. You can read more about tau at tauday.com.
Anyway, back to the game. Build and run to see some glorious rotation in action.
Whoops, something is still not right. The spaceship certainly rotates but it seems to be pointing in the direction opposite to where it’s flying!
Here’s what’s happening: the sprite for the spaceship points straight up, which corresponds to the default rotation value of 0 degrees/radians. But in mathematics, an angle of 0 degrees (or radians) doesn’t point upward, but to the right:
To overcome these differences from sprite orientation, change the line to:
_playerSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90); |
This subtracts 90 degrees (after getting degrees to radians to subtract from the angle in radians) to make the sprite point to the right at an angle of 0 degrees, so that it lines up with the way atan2f()
does things.
Build and run once more to try it out. You’ll finally have a spaceship that has its head on straight!
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.
Unfortunately, having the spaceship get stuck on the edges of the screen isn’t very satisfying. You’re going to fix that by making it bounce off the screen borders instead.
First, comment these lines from updatePlayer
:
//newX = MIN(_winSize.width, MAX(newX, 0));
//newY = MIN(_winSize.height, MAX(newY, 0)); |
Then add the following code to updatePlayer
, just after the commented lines:
BOOL collidedWithVerticalBorder = NO;
BOOL collidedWithHorizontalBorder = NO;
if (newX < 0.0f)
{
newX = 0.0f;
collidedWithVerticalBorder = YES;
}
else if (newX > _winSize.width)
{
newX = _winSize.width;
collidedWithVerticalBorder = YES;
}
if (newY < 0.0f)
{
newY = 0.0f;
collidedWithHorizontalBorder = YES;
}
else if (newY > _winSize.height)
{
newY = _winSize.height;
collidedWithHorizontalBorder = YES;
} |
This checks whether the spaceship hit any of the screen borders and if so, sets a BOOL
variable to YES. But what to do after such a collision takes place? To make the spaceship bounce off the border, you can simply reverse its speed and acceleration.
First, define a constant at the top of the file, right below the const float MaxPlayerSpeed = 200.0f;
line:
const float BorderCollisionDamping = 0.4f; |
Add the following lines in updatePlayer
, directly below the code you last added there:
if (collidedWithVerticalBorder)
{
_playerAccelX = -_playerAccelX * BorderCollisionDamping;
_playerSpeedX = -_playerSpeedX * BorderCollisionDamping;
_playerAccelY = _playerAccelY * BorderCollisionDamping;
_playerSpeedY = _playerSpeedY * BorderCollisionDamping;
}
if (collidedWithHorizontalBorder)
{
_playerAccelX = _playerAccelX * BorderCollisionDamping;
_playerSpeedX = _playerSpeedX * BorderCollisionDamping;
_playerAccelY = -_playerAccelY * BorderCollisionDamping;
_playerSpeedY = -_playerSpeedY * BorderCollisionDamping;
} |
If a collision registered, you flip the acceleration and speed values around. Notice that this also multiplies the acceleration and speed by a damping value, BorderCollisionDamping
.
As usually happens in a collision, some of the movement energy is dissipated by the impact. In this case, you make the spaceship retain only 40% of its speed after bumping into the screen edges.
Try it out. Smash the spaceship into the border and see what happens. Who said spaceships can’t bounce?
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.0f, the spaceship actually gains energy from the collision.
Note: Now why is there an f behind those numbers in the code: 0.4f, 0.1f, 0.0f, and so on? And why did you use atan2f()
instead of just atan2()
? When you write games, you want to work with floating point numbers as much as possible because, unlike integers, they allow for digits behind the decimal point. This allows you to be much more precise.
There are two types of floating point numbers: floats and doubles (there is also a “long double”, but that’s the same as a double on the iPhone). Doubles are more precise than floats but they also take up more memory and are slower in practice. When you don’t put the f behind the number and just use 0.4, 0.1, 0.0, or when you use the version of a math function without the f suffix, you are working with doubles and not floats.
It doesn’t really matter if you use a double here and there. For example, the time value that CACurrentMediaTime()
returns is a double. However, if you’re doing hundreds of thousands of calculations every frame, you will notice the difference. I did a quick test on a couple of my devices and the same calculations using doubles were 1.5 to 2 times slower. So it’s a good habit to stick to regular floats where you can.
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 a constant flicker between the up and down angles.
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 atan2f()
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 our old friend, Pythagoras.
Right now you don’t have such a thing as “the ship’s speed”. Instead, you have two speeds: one in the x-direction and one in the y-direction. But in order to draw any conclusions about “the ship’s speed” – is it too slow to actually rotate the ship? – you need to combine these x and y speed values into one single value:
Here you are in the Know 2 Sides, Need Remaining Side case discussed earlier.
As you can see, the true speed of the spaceship, that is, how many points it moves across the screen in a 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 speed2 = _playerSpeedX
2 + _playerSpeedY
2
To find the actual value, you need to take the square root:
true speed = √(_playerSpeedX
2 + _playerSpeedY
2)
Add this code to updatePlayer
. First remove this block of code:
float angle = atan2f(_playerSpeedY, _playerSpeedX);
_playerSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f); |
And replace it with this block of code:
float speed = sqrtf(_playerSpeedX*_playerSpeedX + _playerSpeedY*_playerSpeedY);
if (speed > 40.0f)
{
float angle = atan2f(_playerSpeedY, _playerSpeedX);
_playerSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f);
} |
Build and run. You’ll see the spaceship now acts a lot more stable at the edges of the screen. If you’re wondering where the value 40.0f came from, the answer is: experimentation. I put some NSLog()
statements into the code to look at the speeds at which the craft typically hit the borders, and then I tweaked 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 happened 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 is the details that make great products.
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.
Blending sounds fancy, but it’s actually quite easy to implement. It does require you to keep track of the spaceship’s angle between updates, so add a new instance variable for it in the implementation block in MyScene.m:
@implementation MyScene
{
...
float _playerAngle;
} |
Change the rotation code in updatePlayer
from this:
float speed = sqrtf(_playerSpeedX*_playerSpeedX + _playerSpeedY*_playerSpeedY);
if (speed > 40.0f)
{
float angle = atan2f(_playerSpeedY, _playerSpeedX);
_playerSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f);
} |
To this:
float speed = sqrtf(_playerSpeedX*_playerSpeedX + _playerSpeedY*_playerSpeedY);
if (speed > 40.0f)
{
float angle = atan2f(_playerSpeedY, _playerSpeedX);
const float RotationBlendFactor = 0.2f;
_playerAngle = angle * RotationBlendFactor + _playerAngle * (1.0f - RotationBlendFactor);
}
_playerSprite.zRotation = _playerAngle - SK_DEGREES_TO_RADIANS(90.0f); |
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 counts for 20% towards the actual rotation that you set on the spaceship. Of course, over time more and more of the new angle gets added so that eventually the spaceship does point in the proper direction.
Note that the line that sets the _playerSprite
’s rotation property is now outside the if
statement.
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 in the opposite direction. It always happens at the same point in the circle. What’s going on?
Well, there is something you should know about atan2f()
. It does not return an angle in the convenient range of 0 to 360 degrees, but a value between +π and –π radians, or between +180 and -180 degrees to us non-mathematicians:
That means if you’re turning counterclockwise, at some point the angle will jump from +180 degrees to -180 degrees; or the other way around if you’re turning clockwise. And that’s where the weird spinning effect happens.
The problem is that when the new angle jumps from 180 degrees to -180 degrees, _playerAngle
is still positive because it is trailing behind a bit. When you blend these two together, the spaceship actually starts turning the other way around. It took me a while to figure out what was causing this!
To fix it, you need to recognize when the angle makes that jump and adjust _playerAngle
accordingly. Add a new instance variable at the bottom of the MyScene
implementation block:
@implementation MyScene
{
...
float _lastAngle;
} |
And change the rotation code one more time from this:
float speed = sqrtf(_playerSpeedX*_playerSpeedX + _playerSpeedY*_playerSpeedY);
if (speed > 40.0f)
{
float angle = atan2f(_playerSpeedY, _playerSpeedX);
const float RotationBlendFactor = 0.2f;
_playerAngle = angle * RotationBlendFactor + _playerAngle * (1.0f - RotationBlendFactor);
}
_playerSprite.zRotation = _playerAngle - SK_DEGREES_TO_RADIANS(90.0f); |
To this:
float speed = sqrtf(_playerSpeedX*_playerSpeedX + _playerSpeedY*_playerSpeedY);
if (speed > 40.0f)
{
float angle = atan2f(_playerSpeedY, _playerSpeedX);
// Did the angle flip from +Pi to -Pi, or -Pi to +Pi?
if (_lastAngle < -3.0f && angle > 3.0f)
{
_playerAngle += M_PI * 2.0f;
}
else if (_lastAngle > 3.0f && angle < -3.0f)
{
_playerAngle -= M_PI * 2.0f;
}
_lastAngle = angle;
const float RotationBlendFactor = 0.2f;
_playerAngle = angle * RotationBlendFactor + _playerAngle * (1.0f - RotationBlendFactor);
}
_playerSprite.zRotation = _playerAngle - SK_DEGREES_TO_RADIANS(90.0f); |
Build and run. That’ll fix things right up and you should have no more problems turning your spacecraft!
This may have seemed like an arcane little problem not worth so much time, but it’s good to be aware that atan2f()
gives you angles from -180 degrees (or –π radians) to +180 degrees (or π radians). If you’re using arc tangent to calculate an angle and you get weird behavior like the spinning you saw here, then you might need to verify that your angles really are what you expect them to be.
Using Trig to Find Your Target
This is a great start – you have a spaceship moving along pretty smoothly! But so far the spaceship’s life is too easy and carefree. Let’s spice this up by adding an enemy: a big cannon!
Add two new instance variables in the implementation block in MyScene.m:
@implementation MyScene
{
...
SKSpriteNode *_cannonSprite;
SKSpriteNode *_turretSprite;
} |
You’ll set these sprites up in initWithSize
. Place this code before the creation of _playerSprite
so that the spaceship always gets drawn “above” the cannon:
_cannonSprite = [SKSpriteNode spriteNodeWithImageNamed:@"Cannon"];
_cannonSprite.position = CGPointMake(_winSize.width/2.0f, _winSize.height/2.0f);
[self addChild:_cannonSprite];
_turretSprite = [SKSpriteNode spriteNodeWithImageNamed:@"Turret"];
_turretSprite.position = CGPointMake(_winSize.width/2.0f, _winSize.height/2.0f);
[self addChild:_turretSprite]; |
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 have a brand-new cannon sitting smack in the middle of the screen.
Now you’ll give the cannon a target to snipe at – I bet you know who!
Yep, its turret should point at the player at all times. To get this to work, you need to figure out the angle to rotate the turret so that it points toward the player.
Figuring this out will be very similar to how you figured out how to rotate the spaceship in order to point toward the direction it’s moving in. The difference is that this time, the triangle won’t be based on the speed of the spaceship. Instead it will be drawn between the center positions of the two sprites:
Again, you can use atan2f()
to calculate this angle. Add the following method:
- (void)updateTurret:(NSTimeInterval)dt
{
float deltaX = _playerSprite.position.x - _turretSprite.position.x;
float deltaY = _playerSprite.position.y - _turretSprite.position.y;
float angle = atan2f(deltaY, deltaX);
_turretSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f);
} |
The deltaX
and deltaY
variables measure the distance between the player sprite and the turret sprite. You plug these values into atan2f()
to get angle of rotation.
As before, you need to convert this angle to include the offset from zero (90.0) so the sprite points correctly. Remember that atan2()
always gives you the angle between the hypotenuse and the 0-degree line. It is not the angle inside the triangle.
Don’t forget to call this new updateTurret:
method from update
, or nothing much will happen:
-(void)update:(NSTimeInterval)currentTime
{
...
[self updatePlayerAccelerationFromMotionManager];
[self updatePlayer:_deltaTime];
[self updateTurret:_deltaTime];
} |
Build and run. The cannon is now always pointing at the spaceship. See how easy that was? That’s the power of trig for you!
Challenge: It is unlikely that a real cannon would be able to act so instantaneously – it would have to be able to predict exactly where the target was going. Instead, a real cannon would always be playing catch up.
You can accomplish this by “blending” the old angle with the new one, just like you did earlier. 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… That Move!
Soon the player will be able to 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 add health bars to the scene using SKNode
, a way to group other node content using Sprite Kit.
First, you have to do some prep. Add a couple of new constants to the top of the file:
const int MaxHP = 100;
const float HealthBarWidth = 40.0f;
const float HealthBarHeight = 4.0f; |
Also add a couple of new instance variables in the implementation block:
@implementation MyScene
{
...
int _playerHP;
int _cannonHP;
SKNode *_playerHealthBar;
SKNode *_cannonHealthBar;
} |
Add the following to the bottom of initWithSize
to set up these new variables:
_playerHealthBar = [SKNode node];
[self addChild:_playerHealthBar];
_cannonHealthBar = [SKNode node];
[self addChild:_cannonHealthBar];
_cannonHealthBar.position = CGPointMake(
_cannonSprite.position.x - HealthBarWidth/2.0f + 0.5f,
_cannonSprite.position.y - _cannonSprite.size.height/2.0f - 10.0f + 0.5f);
_playerHP = MaxHP;
_cannonHP = MaxHP; |
The _playerHealthBar
and _cannonHealthBar
objects are instances of SKNode
, which is like a sprite except that you can draw other sprites and shapes into it. You will be using a SKSpriteNode but drawing directly into using Core Graphics, which is ideal for your health bars that consist of a border and a filled rectangle.
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.
However, whenever the spaceship moves, you have to adjust the position of the _playerHealthBar
as well. That happens in updatePlayer
. Add the following lines to the bottom of that method:
_playerHealthBar.position = CGPointMake(
_playerSprite.position.x - HealthBarWidth/2.0f + 0.5f,
_playerSprite.position.y - _playerSprite.size.height/2.0f - 15.0f + 0.5f); |
Now all that’s left is drawing the bars themselves. Add this new method to the bottom of the file:
-(void) drawHealthBar:(SKNode *)node withName:(NSString *)name andHealthPoints:(int)hp
{
[node removeAllChildren];
float widthOfHealth = (HealthBarWidth - 2.0f)*hp/MaxHP;
UIColor *clearColor = [UIColor clearColor];
UIColor *fillColor = [UIColor colorWithRed:113.0f/255.0f green:202.0f/255.0f blue:53.0f/255.0f alpha:1.0f];
UIColor *borderColor = [UIColor colorWithRed:35.0f/255.0f green:28.0f/255.0f blue:40.0f/255.0f alpha:1.0f];
//create the outline for the health bar
CGSize outlineRectSize = CGSizeMake(HealthBarWidth-1.0f, HealthBarHeight-1.0);
UIGraphicsBeginImageContextWithOptions(outlineRectSize, NO, 0.0);
CGContextRef healthBarContext = UIGraphicsGetCurrentContext();
//Drawing the outline for the health bar
CGRect spriteOutlineRect = CGRectMake(0.0, 0.0, HealthBarWidth-1.0f, HealthBarHeight-1.0f);
CGContextSetStrokeColorWithColor(healthBarContext, borderColor.CGColor);
CGContextSetLineWidth(healthBarContext, 1.0);
CGContextAddRect(healthBarContext, spriteOutlineRect);
CGContextStrokePath(healthBarContext);
//Fill the health bar with a filled rectangle
CGRect spriteFillRect = CGRectMake(0.5, 0.5, outlineRectSize.width-1.0, outlineRectSize.height-1.0);
spriteFillRect.size.width = widthOfHealth;
CGContextSetFillColorWithColor(healthBarContext, fillColor.CGColor);
CGContextSetStrokeColorWithColor(healthBarContext, clearColor.CGColor);
CGContextSetLineWidth(healthBarContext, 1.0);
CGContextFillRect(healthBarContext, spriteFillRect);
//Generate a sprite image of the two pieces for display
UIImage *spriteImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRef spriteCGImageRef = [spriteImage CGImage];
SKTexture *spriteTexture = [SKTexture textureWithCGImage:spriteCGImageRef];
spriteTexture.filteringMode = SKTextureFilteringLinear; //This is the default anyway
SKSpriteNode *frameSprite = [SKSpriteNode spriteNodeWithTexture:spriteTexture size:outlineRectSize];
frameSprite.position = CGPointZero;
frameSprite.name = name;
frameSprite.anchorPoint = CGPointMake(0.0, 0.5);
[node addChild:frameSprite];
} |
This sets up the rectangles for a single health bar. First it sets up the colors for the health bar pieces, then draws it twice: once for the border, which always has the same size, and once for the amount of health, which changes depending on the number of remaining hit points. The method then generates a Sprite Kit Texture of these drawing calls, and finally renders an SKSpriteNode from this texture.
This SKSpriteNode is then added to the SKNode so it gets rendered in the location of the SKNode.
Of course, you need to call this method, once for the player and once for the cannon. Add these two lines to the bottom of update
:
[self drawHealthBar:_playerHealthBar withName:@"playerHealth" andHealthPoints:_playerHP];
[self drawHealthBar:_cannonHealthBar withName:@"cannonHealth" andHealthPoints:_cannonHP]; |
Build and run. Now both the player and the enemy have health bars:
Using Trig for Collision Detection
Right now, the spaceship can fly directly over the cannon without consequence. It would be more fun if it didn’t have it so easy and instead suffered damage for colliding with the cannon. This is where this tutorial enters the sphere of collision detection (don’t miss that pun).
At this point, a lot of game devs think, “I need a physics engine!” and start using Sprite Kit’s built-in physics engine. While it’s certainly true that you can use Sprite Kit’s physics engine for this, it’s not that hard to do collision detection yourself, especially when you can use circles.
Detecting whether two circles intersect is a piece of cake: all you have to do is look at the distance between them (*cough* Pythagoras) and see if it is smaller than the radius of both circles.
Add two new constants to the top of MyScene.m:
const float CannonCollisionRadius = 20.0f;
const float PlayerCollisionRadius = 10.0f; |
These are the sizes of the collision circles around the cannon and the player. Looking at the sprite, you’ll see that the radius of the cannon is slightly larger at 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 will think it is unfair.
The fact that the spaceship isn’t circular at all shouldn’t deter you. Often a circle is a good enough approximation of the shape of your sprite, and it has the big advantage that it makes it easy to do the necessary 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 that does the collision detection:
- (void)checkCollisionOfPlayerWithCannon
{
float deltaX = _playerSprite.position.x - _turretSprite.position.x;
float deltaY = _playerSprite.position.y - _turretSprite.position.y;
float distance = sqrtf(deltaX*deltaX + deltaY*deltaY);
if (distance <= CannonCollisionRadius + PlayerCollisionRadius)
{
[self 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-distance. With these two values, you can calculate the hypotenuse, which is the true distance between these sprites. If that distance is smaller than the collision circles, a sound effect is played.
In update
, add a call to this new method, above the calls to drawHealthBar
:
[self checkCollisionOfPlayerWithCannon]; |
Add this line to initWithSize
to pre-load the sound effect:
_collisionSound = [SKAction playSoundFileNamed:@"Collision.wav" waitForCompletion:NO]; |
Declare your intent to use the AVFoundation framework and bring it in through the new @import syntax. Insert the following line, right after your @import CoreMotion;
at the top:
Finally, declare an instance variable to for the SKAction
sound effect by adding this line in the implementation block:
SKAction *_collisionSound; |
Give it a whirl and drive the spaceship into the cannon.
Notice that the sound effect is a little odd. It’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 many, and it plays the sound effect for every one of them.
Collision detection is only the first step. The second step is collision response. Not only do you want to play a sound effect, but you also want the spaceship to bounce off the cannon.
Add these lines inside the if
statement in checkCollisionOfPlayerWithCannon
:
const float CannonCollisionDamping = 0.8f;
_playerAccelX = -_playerAccelX * CannonCollisionDamping;
_playerSpeedX = -_playerSpeedX * CannonCollisionDamping;
_playerAccelY = -_playerAccelY * CannonCollisionDamping;
_playerSpeedY = -_playerSpeedY * CannonCollisionDamping; |
This is similar to what you did for making the spaceship bounce off the screen borders. Build and run to see how it works.
It works pretty well when the spaceship is going fast when it hits the cannon. But if it’s moving too slow, then even after reversing the speed, the ship stays within the collision radius and never makes its way out of it. So this solution has some problems.
Instead of bouncing the ship off the cannon by reversing its existing speed, you will simply expel the ship with a fixed speed so that it always moves outside of the collision area, no matter how fast or slow it was flying when it hit the cannon.
This requires you to add a new constant at the top of MyScene.m:
const float CannonCollisionSpeed = 200.0f; |
Remove those five lines you just added in checkCollisionOfPlayerWithCannon
and replace them with the following:
float angle = atan2f(deltaY, deltaX);
_playerSpeedX = cosf(angle) * CannonCollisionSpeed;
_playerSpeedY = sinf(angle) * CannonCollisionSpeed;
_playerAccelX = 0.0f;
_playerAccelY = 0.0f;
_playerHP = MAX(0, _playerHP - 20);
_cannonHP = MAX(0, _cannonHP - 5); |
That is the speed you want the spaceship to have after it collides with the cannon. You need to calculate how much of that speed goes in the x-direction and how much in the y-direction, based on the angle of impact:
You can calculate the angle using arctan because you know the current speed of the player. To calculate how fast the player bounces off the cannon in the x- and y-directions, you already have two pieces of information: the angle that you just calculated, and the new speed, CannonCollisionSpeed
, which is the hypotenuse of the triangle.
If you look back at the theory section, you will see that you can calculate the new x-component of the speed using the cosine function and the y-component using the sine function:
- new x-speed = cos(angle) * hypotenuse
- new y-speed = sin(angle) * hypotenuse
And that’s exactly what you’re doing in the code above. Build and run, and you’ll see the spaceship now bounces properly off the cannon and loses some hit points in the process.
Adding Some Spin
For an additional effect, you can add 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 dizzy). Add a new instance variable in the implementation block:
@implementation MyScene
{
...
float _playerSpin;
} |
In checkCollisionOfPlayerWithCannon
, add the following line inside the collision detection block:
if (distance <= CannonCollisionRadius + PlayerCollisionRadius)
{
...
_playerSpin = 180.0f * 3.0f;
} |
This sets the amount of spin to a circle and a half, which I think looks pretty good. Now at the bottom of updatePlayer
, add the following code to add spin to the rotation:
_playerSprite.zRotation += -SK_DEGREES_TO_RADIANS(_playerSpin);
if (_playerSpin > 0.0f)
{
_playerSpin -= 2.0f * 360.0f * dt;
if (_playerSpin < 0.0f)
{
_playerSpin = 0.0f;
}
} |
The amount of spin quickly decreases over time – I chose a speed of 720 degrees per second. Once the spin has reached 0, it stops.
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.
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.
In the meantime, drop by the forums to share how it’s going for you so far! Or if you really can’t wait until part 2, why not brush up on some more Sprite Kit with some of our other tutorials. Alternatively, if you want to start diving into more game development why not take a look at our book, iOS Games by Tutorials.
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 Game Programming [Sprite Kit Version]: Part 1/2 is a post from: Ray Wenderlich
The post Trigonometry for Game Programming [Sprite Kit Version]: Part 1/2 appeared first on Ray Wenderlich.