Trigonometry for Game Programming [Sprite Kit Version]: Part 2/2

Learn about trigonometry functions like sin, cos, and tan while you make a fun iOS space game! By Tony Dahbura.

Leave a rating/review
Save for later
Share

Contents

Hide contents

Trigonometry for Game Programming [Sprite Kit Version]: Part 2/2

40 mins

Getting Started

Hitting Your Targets

Challenges for the 1337

Adding an Orbiting Shield

Game Over, With Trig!

Where to Go from Here?

Learn Trigonometry for Game Programming!

Update Note: Check out the updated version of this trigonometry tutorial, now powered by Swift and running on iOS 8!

Welcome back to the Trigonometry for Game Programming series!

In the first part of the series, you learned the basics of trigonometry and experienced for yourself how useful it is for making games. You saw that math doesn’t have to be boring – as long as you have a fun project to apply it to, such as making your own games.

Oh yes, and you built the foundation for a solid little space shooter game!

In this second and final part of the series, you will add missiles, an orbiting asteroid shield and an animated “game over” screen to your game. Along the way, you’ll learn more about sine and cosine and see some other useful ways to put the power of trig to work in your games.

Get ready to ride the sine wave back into space!

This tutorial picks up where you left off in the last part. If you don’t have it already, here is the project with all of the source code up to this point.

As of right now, your game has a spaceship and a rotating cannon, each with health bars. While they may be sworn enemies, neither has the ability to damage the other, unless the spaceship flies right into the cannon.

It’s time for some fireworks. You will now give the player the ability to fire missiles at the cannon by swiping the screen. The spaceship will then launch a missile in the direction of that swipe. For that, you first need to enable touch handling on the layer.

Open the Xcode project and add these instance variables to MyScene.m:

Also add these lines at the bottom of the code inside initWithSize:

You initially set the missile sprite to be hidden and only make it visible when the player fires.

Now add the methods to handle the touches, starting with the one that is called whenever the user first puts her finger on the touchscreen:

This is pretty simple – you store the touch location and the time of the touch in the new instance variables.

The actual work happens in touchesEnded, which you add next:

This determines whether there hasn’t been too much time between starting and ending the swipe, 0.3 seconds at most (otherwise, the missle doesn’t fire). Also, the player can only shoot one missile at a time, so the touch is ignored if a missile is already flying.

The last bit figures out what sort of gesture the user made: was it really a swipe, or just a tap? You only launch missiles on swipes, not taps.

Note: You have done this sort of calculation a couple of times already – subtract two coordinates, then use the Pythagorean Theorem to find the distance between them.

It is common in games to combine the x and y values into one type such as CGPoint. Such a value is referred to as a vector, a topic large enough that it deserves its own tutorial.

There are two ways you could make the missile fly.

The first would be to create new _playerMissileSpeedX and Y instance variables and fill them in with the correct speed values, based on the angle that you’re aiming the missile. Inside update, you would then add this speed to the missile sprite’s position, and check whether the missile flew outside of the visible screen so the player can fire again. This is very similar to how you made the spaceship move in Part 1 of this tutorial.

However, the missile doesn’t need to change course on its way: it always flies in a straight line. So you can take an alternative approach: calculating the destination point when you launch the missile. Then you can set a moveTo action on the missile sprite and let Sprite Kit handle the rest. This also saves you from having to check whether the missile has left the visible screen.

I bet you saw this coming: the second approach is exactly what you are going to implement right now, and you will use trig to do it.

To begin, add the following lines to the TODO section in touchesEnded:

This is nothing fancy. You calculate the angle, set up the sprite’s rotation and position, and make the missile sprite visible.

Now, however, comes the math. You know the starting position of the missile (which is the position of the player’s ship) and you know the angle (derived from the player’s swipe motion). What you need to calculate now is the destination point of the missile based on these facts.

There are actually four different situations that you need to handle, so let’s first consider the one where the user swiped in a downward motion:

The destination point always lies just outside the screen instead of exactly on the edge of the screen, because it looks better if the missile completely flies out of sight. For this purpose, add a constant at the top of MyScene.m and call it Margin:

To find the destination point, you need more than just the angle. Fortunately, if you know that the player is shooting downward, you also know the vertical distance the missile needs to fly. That is simply the starting y-position of the missile, plus the margin. (Remember that the bottom of the screen is y = 0 in Sprite Kit.)

Now you have the angle and the adjacent side. To get the destination point, you simply need to calculate the opposite side of the triangle and add it to the starting x-position of the missile.

That sounds like a job for the tan() function – not the arc tangent that you’ve been using up to now, but the normal one:

tan(angle) = opposite / adjacent

Or for your purposes:

opposite = tan(angle) * adjacent

There’s only one snag. The angle that is pictured in the above diagram isn’t really the angle you measured, because the angle you get back from atan2f() is always relative to the 0 degree line of the circle:

Note that in this particular diagram, the adjacent and opposite sides are swapped – it is now the adjacent you’re looking for. So the formula becomes:

adjacent = opposite / tan(angle)

You could probably make this work, but that division makes me cringe. What if tan(angle) happens to be 0? Then you’ll have a division-by-zero on your hands. I’d much rather stick to the first formula, the one that calculates the opposite.

It turns out that this is actually no problem if you remember that beta = (90 – alpha). In other words, if you position yourself in the opposite corner of the triangle and use an angle of (90 – alpha), then you can use that first formula without problems.

Be careful: now that the opposite side points the other way around, you need to subtract it rather than add it to the starting x-position.

Let’s put all this in code. Modify the touchesEnded method to look like the following:

Add the following line to the end of the @implemenation MyScene section:

Also add this line at the bottom of the code inside initWithSize method:

This does two things:

Build and run, and try it out. Move the ship so that it is about halfway centered on the screen and then swipe down. You should see a missile fly along the path that you swiped. Note that you can only fire one missile at a time – the player should have to wait until the previous missile has disappeared from the screen before firing again.

If you try swiping in any other direction, you’ll notice that the missile doesn’t quite fly where you want. That’s because currently you have only handled the situation where the destination point lies below the bottom of the screen. Each of the other screen edges requires slightly different calculations.

Replace the code that calculates the destination point (the “// 1” section) with this:

You have actively divided the screen into four sections, like slices of a pie:

Each of these sections calculates a slightly different destination point. Notice that the code for shooting left comes last, because here you have the issue of switching from +180 to -180 degrees again (recall that this is the range of values that atan2() returns).

Now you should be able to fire in any direction! Build and run and give it a try.

Shooting in any direction

There is still one problem (how could there not be?). Sometimes the missile appears to be going faster than at other times.

That's because currently, the duration of the animation is hard-coded to last 2 seconds. If the missile needs to travel far, then it goes faster in order to cover more distance in the same amount of time.

Instead what you want is for each missile to always travel at the same speed. The hypotenuse comes to the rescue once more!

First, add a new constant at the top of MyScene.m:

This is the distance that you want the missile to travel per second. By calculating the length of the hypotenuse, you know the actual distance that the missile travels. To get the needed duration of the animation, you divide those two distances.

Sounds complicated? Nah…

In touchesEnded, change the code that creates the missileMoveAction action (the // 2 section) to the following:

That’s all there is to it. Build and run the app again. Now the missile always flies at the same speed, no matter how far or close the destination point is.

And that’s how you use trig to set up a moveTo action. It’s a bit involved, but then it’s largely fire & forget because Sprite Kit does all the work of animating for you.

Meme - not bad

Right now, the missile completely ignores the cannon – your spaceship might as well be firing a beam of green light!

That’s about to change. As before, you will use a simple radius-based method of collision detection.

Add a new constant to the source at the top of MyScene.m:

For simplicity’s sake, you will assume that the missile itself doesn’t have a radius, so whenever its center point goes within the cannon hit radius, that will be sufficient to register as a collision. That’s precise enough for this game.

In preparation to play the hit sound, add an instance variable at the bottom of the implementation for the sound action:

And initialize the SKAction in the initWithSize method, by adding this line at the bottom:

Now, add a new method:

This should be old hat by now: you calculate the distance and then play a sound effect via a Sprite Kit SKAction when the distance has become smaller than the hit radius.

Note that you need to call removeAllActions on the missile sprite, because you want it to stop moving. This tells the moveTo SKaction that it is no longer needed.

Call this new method from within update. Place it directly below the call to updatePlayer.

Build and run, then try it out. Finally you can inflict some damage on the enemy!

Inflicting damage

Here's a challenge for you: can you make the cannon shoot back at the spaceship?

Try to see if you can figure it out - you already know all the required pieces, and this will be some really good practice to make sure you understand what we've covered so far.

Try it out for yourself before you look at the solution!

[spoiler title="Make the cannon shoot back"]

To implement this on your own, all you have to do is create a new missile sprite (using CannonMissile.png), calculate the destination, and send the missile on its way. You know the angle because the turret already points at the player. The destination point for the moveTo action is obviously the position of the player at that moment.

Collision detection with the player works the same way as before: the missile has hit as soon as the distance between the missile and the player becomes less than a certain radius. To make the game extra challenging for the player, allow the cannon to shoot more than one missile at a time.

[/spoiler]

 

Got that, and think you're some hot stuff? Here's another challenge for you!

Currently your missiles fly to their destination point in a straight line. But what if the missiles were heat-seeking? A heat-seeking missile adjusts its course when it detects that the player has moved.

You've got the power of trig on your side, so how would you do it? Hint: instead of calculating the speed and direction of the missile just once, you would do it again and again on each frame. Give it a try.

[spoiler title="Make the missile heat seeking"]

Since you're calculating the speed and direction of the missile on each frame, you can no longer use a moveTo action. Instead, you have to do the animation by yourself. Continuously adjust the speed of the missile based on the new angle that it makes with the player. To read more about this sort of "seeking" behavior, check out the Game AI tutorial.

Make sure to give the guided missile a limited lifetime, so the player can avoid it if he keeps dodging it long enough, or the game might become a bit too hard to play!

[/spoiler]

 

How'd you do? Is your spaceship dodging guided missiles like Tom Cruise, or still flying around scot-free?

To make the game more challenging, you will give the enemy a shield. The shield will be a magical asteroid sprite that orbits the cannon and destroys any missiles that come near it.

Add a couple more constants to the top of MyScene.m:

And some new instance variables as well:

I added the “in degrees” comments above because unlike the other angles, which were in radians, here you’ll work with degrees. They are just easier to wrap your head around.

Initialize the new sprite inside initWithSize after all the previous code:

And add a new method:

The asteroid will orbit around the cannon. In other words, it describes a circular path, round and round and round and round. To accomplish this, you need two pieces: the radius that determines how far the asteroid is from the center of the cannon, and the angle that describes how far it has rotated around that center point.

This is what updateOrbiter does:

You have briefly seen sinf() and cosf() in action, but it may not have been entirely clear how they worked. Sure, you have the formulas memorized (really? :]) and you know that both of these functions can be used to calculate the lengths of the other sides, once you have an angle and the hypotenuse.

But aren’t you curious why you can actually do that? I thought you were!

Let’s draw a circle:

The illustration above exactly depicts the situation of the asteroid orbiting around the cannon. The circle describes the path of the asteroid and the origin of the circle is the center of the cannon.

The angle starts at zero degrees but increases all the time until it ends up right back at the beginning. As you can see it, is the radius of the circle that determines how far away from the center the asteroid is placed.

So, given the angle and the radius, you can derive the x- and y-positions using the cosine and sine, respectively:

Now let’s take a look at a plot of a sine wave and a cosine wave:

On the horizontal axis are the degrees of a circle, from 0 to 360, or 0 to 2π radians if you’re a mathematician. The vertical axis usually goes from -1 to +1, but if your circle has a radius that is greater than one (and they tend to) then the vertical axis really goes from –radius to +radius.

As the angle increases from 0 to 360, find the angle on the horizontal axis in the plots for the cosine and sine waves. The vertical axis then tells you what the values for x and y are:

Make sense? Awesome. Did you also notice that the curves of the sine and cosine are very similar? In fact, the cosine wave is simply the sine wave shifted by 90 degrees. Go ahead and impress your friends and family with your knowledge of the mathematical origins of sine and cosine. :]

Back to coding. Add a call to updateOrbiter at the bottom of update:

Build and run the app. You should now have an asteroid that perpetually circles the enemy cannon.

You can also make the asteroid spin around its own axis. Add the following line to the bottom of updateOrbiter:

By setting the rotation to _orbiterAngle, the asteroid always stays oriented in the same position relative to the cannon, much like the moon always shows the same side to the earth. Even though it looks like it isn’t spinning, it certainly is!

Insert a minus sign to give the asteroid extra spin that you can see. Pick whichever effect you like best. Build and run to play around with it for a bit.

Let’s give the orbiter a purpose. If the missile comes too close, the asteroid will destroy it before it gets a chance to do any damage to the cannon. Add the following at the bottom of updateOrbiter:

No surprises for you here. It’s the same code you’ve seen several times now. Just remember to stop the moveTo action that is on the missile sprite. For added visual effect, you also scale the asteroid sprite momentarily. This makes it look like the orbiter “ate” the missile.

Build and run to see your new orbiting shield in action.

Orbiting missile shield

There is still more that you can do with sines and cosines. They’re not just useful for calculating things with triangles – they also come in handy for animations.

A good place to show an example of such an animation is the game over screen. Add a few new instance variables in the implementation block at the top of MyScene.m:

And a new method:

This method both checks whether the game is done, and if so, handles the game over animation:

Call checkGameOver at the bottom of update:

And add a small snippet of logic to the top of touchesEnded:

This restarts the game when the user taps on the game over screen.

Build and run, then try it out. Shoot at the cannon or collide your ship with it until one of you runs out of hit points. The screen will fade to black and the game over text will appear:

Game over

The game no longer responds to the accelerometer, but the animations still keep going. That gives it a nice twist!

This is all fine and dandy, but where are the sine and cosines? As you may have noticed, the fade in animation of the black layer was very linear. It just goes from transparent to opaque in a conventional fashion.

You can be better than conventional – you can use sinf() to subtly change the timing. This is often known as “easing” and the effect you will apply in particular is known as an “ease out”.

Add the new constant at the top of MyScene.m:

Next, change the code in the else statement in checkGameOver to:

_gameOverElapsed keeps track of how much time has passed since the game ended. It takes two seconds to fade in the black layer (DarkenDuration). The variable t determines how much of that duration has passed by. It always has a value between 0.0 and 1.0, regardless of how long DarkenDuration really is.

Then you perform the magic trick:

This converts t from a linear interpolation into one that breathes a bit more life into things:

Build and run to see the new “ease out” effect. If you find it hard to see the difference, then try it with that “magic” line commented out, or change the duration of the animation. The effect is subtle, but it's there.

Note: If you want to play with the values and test things out quick you can comment out the return in the if (_playerHP > 0 && _cannonHP > 0) // not game over yet block.

There is one more thing I’d like to show you. Let’s make the game over text bounce, because things that bounce are always more fun.

Inside that else clause in checkGameOver, add the following so that the method looks like so:

OK, what's happening here? Recall what a cosine looks like:

If you take the absolute value of cosf() – using fabsf() – then the section that would previously go below zero is flipped. The curve already looks like something that bounces, don’t you think?

Because the output of these functions lies between 0.0 and 1.0, you multiply it by 50 to stretch it out a little. The argument to cosf() is normally an angle, but you’re giving it the _gameOverElapsed time to make the cosine move forward through its curve.

The factor 3.0 is just to make it go a bit faster. You can tinker with these values until you have something that you think looks cool.

Build and run to check out the bouncing text:

bouncing text

You’ve used the shape of the cosine to describe the bouncing motion of the text label. These cosines are useful for all sorts of things!

One last thing you can do is let the bouncing motion lose height over time. You do this by adding a damping factor. Create a new instance variable in the MyScene implementation block:

In checkGameOver, set _gameOverDampen. Do this at the beginning of the if block, like so:

In the else block, change the code underneath the "// Game Over Label Position" comment to be the following:

It’s mostly the same as before, but you multiply the y-value with the damping factor and, simultaneously, you reduce this damping factor slowly from 1.0 back to 0.0 (but never less than 0; that’s what the fmaxf() prevents). Build and run, then try it out!

Here is the full example project from this Trigonometry for Game Programming tutorial series.

Congratulations, you have peered even deeper into the true natures of sine, cosine and tangent, and you have tried them out to see some examples of how they're useful in a real game. I hope you've seen how handy Trigonometry really is for games!

Note that this series didn’t talk so much about arcsin and arccos. They are much less useful in practice than arctan. One common use for arccos is to find the angle between two arbitrary vectors – for example, to model the reflection of a light beam in a mirror, or to calculate how bright an object should be depending on its angle to a light source.

You can find another great example of the usefulness of trigonometric functions in the Tiny Wings tutorial. It uses cosine to give the hills from the game nicely curved shapes.

If you fancy using your new found skills for more game development, but don't know where to start, then why not try out our book iOS Games by Tutorials. It will certainly kick start your development!

Drop by the forums to share your successes and agonies with trig. And use your new powers wisely!

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.

  • If the angle is 0 degrees, then cos(0) is 1*radius but sin(0) is 0*radius. That corresponds exactly to the (x, y) coordinate in the circle: x is equal to the radius, but y is 0.
  • If the angle is 45 degrees, then cos(45) is 0.707*radius and so is sin(45). This means x and y are both the same at this point on the circle. (Note: if you’re trying this out on a calculator, then switch it to DEG mode first. You’ll get radically different answers if it’s in RAD mode, no pun intended.)
  • If the angle is 90 degrees, then cos(90) is 0*radius and sin(90) is 1*radius. You’re now at the top of the circle where the (x, y) coordinate is (0, radius).
  • And so on… To get a more intuitive feel for how the coordinates in the circle relate to the values of the sine, cosine and even tangent functions, try out this cool interactive circle.
  1. It adjusts the angle so you’re looking at it from the other corner. It then calculates the length of the adjacent side, and uses the tangent function to find the length of the opposite side. Finally, it calculates the destination coordinate.
  2. It creates a sequence of actions (each runs one after the other), starting with a sound action, followed by moveTo. Once the sprite has reached its destination, the missileDoneMoveAction action will make the sprite invisible again.
  1. It increments the angle by a certain speed (from the OrbiterSpeed constant), adjusted for the delta time. Because we as humans like to think of angles as anything between 0 and 360 degrees, you use fmodf() to wrap the angle around back to 0 once it becomes greater than 360. You cannot use the % operator for this, because it only works on integers, but fmodf() does the same thing for floats (and fmod() does the same thing for doubles). Wrapping the angles isn’t strictly necessary but it helps you stay sane when you’re trying to debug something like this.
  2. It calculates the new x- and y-positions for the orbiter using the sine and cosine functions. These take the radius (which forms the hypotenuse of the triangle) and the current angle, and return the adjacent and opposite sides, respectively. More about this in a second.
  3. It sets the new position of the sprite by adding the x- and y-positions to the center position of the cannon.
  1. The game keeps on going until either the player or the cannon runs out of hit points.
  2. When the game is over, set _gameOver to YES and disable the accelerometer.
  3. Create a new, all-black color layer and add it on top of everything else. Set its opacity to 0 so that it is completely see-through. Elsewhere in this method you will animate the opacity value of this layer so that it appears to fade in.
  4. Add a new text label and place it on the screen. The text is either “Victory!” if the player won or “Game Over” if the player lost.
  5. The above steps only happen once to set up the game over screen – every time after that, the code enters the else clause. Here you animate the opacity of the new color layer from 0 to 200 – almost completely opaque, but not quite.

Learn Trigonometry for Game Programming!

Note: You have done this sort of calculation a couple of times already – subtract two coordinates, then use the Pythagorean Theorem to find the distance between them.

It is common in games to combine the x and y values into one type such as CGPoint. Such a value is referred to as a vector, a topic large enough that it deserves its own tutorial.

Note: If you want to play with the values and test things out quick you can comment out the return in the if (_playerHP > 0 && _cannonHP > 0) // not game over yet block.

@implemenation MyScene
{
    ...
    SKSpriteNode *_playerMissileSprite;
    CGPoint _touchLocation;
    CFTimeInterval _touchTime;
}
_playerMissileSprite = [SKSpriteNode spriteNodeWithImageNamed:@"PlayerMissile"];
_playerMissileSprite.hidden = YES;
[self addChild:_playerMissileSprite];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint location = [touch locationInNode:self];
    _touchLocation = location;
    _touchTime = CACurrentMediaTime();
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (CACurrentMediaTime() - _touchTime < 0.3 && _playerMissileSprite.hidden)
    {
        UITouch *touch = [touches anyObject];
        CGPoint location = [touch locationInNode:self];
        CGPoint diff = CGPointMake(location.x - _touchLocation.x, location.y - _touchLocation.y);
        float diffLength = sqrtf(diff.x*diff.x + diff.y*diff.y);
        if (diffLength > 4.0f)
        {
            // TODO: more code here
        }
    }
}
 float angle = atan2f(diff.y, diff.x);
 _playerMissileSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f);
 
 _playerMissileSprite.position = _playerSprite.position;
 _playerMissileSprite.hidden = NO;
const float Margin = 20.0f;
if (diffLength > 4.0f)
{
    float angle = atan2f(diff.y, diff.x);
    _playerMissileSprite.zRotation = angle - SK_DEGREES_TO_RADIANS(90.0f);

     _playerMissileSprite.position = _playerSprite.position;
     _playerMissileSprite.hidden = NO;

    float adjacent, opposite;
    CGPoint destination;

    // 1
    angle = M_PI_2 - angle;
    adjacent = _playerMissileSprite.position.y + Margin;
    opposite = tanf(angle) * adjacent;
    destination = CGPointMake(_playerMissileSprite.position.x - opposite, -Margin);

    // 2
    //set up the sequence of actions for the firing
    SKAction *missileMoveAction = [SKAction moveTo:destination duration:2.0f];
    SKAction *missileDoneMoveAction = [SKAction runBlock:(dispatch_block_t)^() {
        _playerMissileSprite.hidden = YES;
    }];
    SKAction *moveMissileActionWithDone = [SKAction sequence:@[_missileShootSound, missileMoveAction, missileDoneMoveAction]];

    [_playerMissileSprite runAction:moveMissileActionWithDone];
}  
SKAction *_missileShootSound;
_missileShootSound = [SKAction playSoundFileNamed:@"Shoot.wav" waitForCompletion:NO];
if (angle <= -M_PI_4 && angle > -3.0f * M_PI_4)
{
    // Shoot down
    angle = M_PI_2 - angle;
    adjacent = _playerMissileSprite.position.y + Margin;
    opposite = tanf(angle) * adjacent;
    destination = CGPointMake(_playerMissileSprite.position.x - opposite, -Margin);
}
else if (angle > M_PI_4 && angle <= 3.0f * M_PI_4)
{
    // Shoot up
    angle = M_PI_2 - angle;
    adjacent = _winSize.height - _playerMissileSprite.position.y + Margin;
    opposite = tanf(angle) * adjacent;
    destination = CGPointMake(_playerMissileSprite.position.x + opposite, _winSize.height + Margin);
}
else if (angle <= M_PI_4 && angle > -M_PI_4)
{
    // Shoot right
    adjacent = _winSize.width - _playerMissileSprite.position.x + Margin;
    opposite = tanf(angle) * adjacent;
    destination = CGPointMake(_winSize.width + Margin, _playerMissileSprite.position.y + opposite);
}
else  // angle > 3.0f * M_PI_4 || angle <= -3.0f * M_PI_4
{
    // Shoot left
    adjacent = _playerMissileSprite.position.x + Margin;
    opposite = tanf(angle) * adjacent;
    destination = CGPointMake(-Margin, _playerMissileSprite.position.y - opposite);
} 
const float PlayerMissileSpeed = 300.0f;
float hypotenuse = sqrtf(adjacent*adjacent + opposite*opposite);
NSTimeInterval duration = hypotenuse / PlayerMissileSpeed;
            
//set up the sequence of actions for the firing
SKAction *missileMoveAction = [SKAction moveTo:destination duration:duration];
const float CannonHitRadius = 25.0f;
SKAction *_missileHitSound;
_missileHitSound = [SKAction playSoundFileNamed:@"Hit.wav" waitForCompletion:NO];
- (void)updatePlayerMissile:(NSTimeInterval)dt
{
    if (!_playerMissileSprite.hidden)
    {
        float deltaX = _playerMissileSprite.position.x - _turretSprite.position.x;
        float deltaY = _playerMissileSprite.position.y - _turretSprite.position.y;
        
        float distance = sqrtf(deltaX*deltaX + deltaY*deltaY);
        if (distance < CannonHitRadius)
        {
            [self runAction:_missileHitSound];
            
            _cannonHP = MAX(0, _cannonHP - 10);
            
            _playerMissileSprite.hidden = YES;
            [_playerMissileSprite removeAllActions];
        }
    }
}
[self updatePlayerMissile:_deltaTime];
const float OrbiterSpeed = 120.0f;  // degrees per second
const float OrbiterRadius = 60.0f;  // degrees
const float OrbiterCollisionRadius = 20.0f;
@implementation MyScene
{
    ...
    SKSpriteNode *_orbiterSprite;
    float _orbiterAngle;  // in degrees
}
_orbiterSprite = [SKSpriteNode spriteNodeWithImageNamed:@"Asteroid"];
[self addChild:_orbiterSprite];
- (void)updateOrbiter:(NSTimeInterval)dt
{
     // 1
    _orbiterAngle += OrbiterSpeed * dt;
    _orbiterAngle = fmodf(_orbiterAngle, 360.0f);
    
    // 2
    float x = cosf(SK_DEGREES_TO_RADIANS(_orbiterAngle)) * OrbiterRadius;
    float y = sinf(SK_DEGREES_TO_RADIANS(_orbiterAngle)) * OrbiterRadius;
    
    // 3
    _orbiterSprite.position = CGPointMake(_cannonSprite.position.x + x, _cannonSprite.position.y + y);
}
-(void)update:(NSTimeInterval)currentTime
{
    ...
    [self updateOrbiter:_deltaTime];
}
_orbiterSprite.zRotation = SK_DEGREES_TO_RADIANS(_orbiterAngle);
if (!_playerMissileSprite.hidden)
{
    float deltaX = _playerMissileSprite.position.x - _orbiterSprite.position.x;
    float deltaY = _playerMissileSprite.position.y - _orbiterSprite.position.y;
        
    float distance = sqrtf(deltaX*deltaX + deltaY*deltaY);
    if (distance < OrbiterCollisionRadius)
    {
        _playerMissileSprite.hidden = YES;
        [_playerMissileSprite removeAllActions];
            
        _orbiterSprite.scale = 2.0f;
        [_orbiterSprite runAction:[SKAction scaleTo:1.0f duration:0.5f]];
    }
}
@implementation MyScene
{
    ...
    SKLabelNode *_gameOverLabel;
    SKSpriteNode *_darkenLayer;
    BOOL _gameOver;
    CFTimeInterval _gameOverElapsed;
}
- (void)checkGameOver:(NSTimeInterval)dt
{
    // 1
    if (_playerHP > 0 && _cannonHP > 0)  // not game over yet
    {
        return;
    }
    
    if (!_gameOver)
    {
        // 2
        _gameOver = YES;
        _gameOverElapsed = 0.0;
        [self stopMonitoringAcceleration];
        
        // 3
        UIColor *fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:1.0f];
        _darkenLayer = [SKSpriteNode spriteNodeWithColor:fillColor size:_winSize];
        _darkenLayer.alpha = 0.0;
        _darkenLayer.position = CGPointMake(_winSize.width/2.0f, _winSize.height/2.0f);
        [self addChild:_darkenLayer];
        
        // 4
        NSString *text = (_playerHP == 0) ? @"GAME OVER" : @"Victory!";
        _gameOverLabel = [[SKLabelNode alloc] initWithFontNamed:@"Helvetica"];
        _gameOverLabel.text = text;
        _gameOverLabel.fontSize = 24.0f;
        _gameOverLabel.position = CGPointMake(_winSize.width/2.0f + 0.5f, _winSize.height/2.0f + 50.0f);
        [self addChild:_gameOverLabel];
    }
    else
    {
        // 5
        if (_darkenLayer.alpha < 200)
        {
            float newOpacity = fminf(200.0f, _darkenLayer.alpha + 255.0f * dt);
            _darkenLayer.alpha = newOpacity;
        }       
    }
}
-(void)update:(NSTimeInterval)currentTime
{
    ...
    [self checkGameOver:_deltaTime];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (_gameOver)
    {

        SKScene *scene = [[MyScene alloc] initWithSize:self.size];
        SKTransition *reveal = [SKTransition flipHorizontalWithDuration:1.0];
        [self.view presentScene:scene transition:reveal];
        return;
    }
    ...
}
const CFTimeInterval DarkenDuration = 2.0;
- (void)checkGameOver:(NSTimeInterval)dt
{
    ...
    }
    else 
    {
        _gameOverElapsed += dt;
        if (_gameOverElapsed < DarkenDuration)
        {
            float t = _gameOverElapsed / DarkenDuration;
            t = sinf(t * M_PI_2);  // ease out
            _darkenLayer.alpha = (200.0f * t)/255.0;
        }
    }
}
t = sinf(t * M_PI_2);  // ease out
- (void)checkGameOver:(NSTimeInterval)dt
{
    ...
    }
    else 
    {
        _gameOverElapsed += dt;
        if (_gameOverElapsed < DarkenDuration)
        {
            float t = _gameOverElapsed / DarkenDuration;
            t = sinf(t * M_PI_2);  // ease out
            _darkenLayer.alpha = (200.0f * t)/255.0;
        }
        // Game Over Label Position
        float y = fabsf(cosf(_gameOverElapsed * 3.0f)) * 50.0f;
        _gameOverLabel.position = CGPointMake(_gameOverLabel.position.x, _winSize.height/2.0f + y);    }
}
@implementation MyScene
{
    ...
    float _gameOverDampen;
}
- (void)checkGameOver:(NSTimeInterval)dt
{
    ...
    if (!_gameOver)
    {
        // 2
        _gameOver = YES;
        _gameOverDampen = 1.0f;
        ...
    }
    ...
}
float y = fabsf(cosf(_gameOverElapsed * 3.0f)) * 50.0f * _gameOverDampen;
_gameOverDampen = fmaxf(0.0f, _gameOverDampen - 0.3f * dt);
_gameOverLabel.position = CGPointMake(_gameOverLabel.position.x, _winSize.height/2.0f + y);
Tony Dahbura

Contributors

Tony Dahbura

Author

Over 300 content creators. Join our team.