How To Make A Multi-directional Scrolling Shooter – Part 2

This is a multi-part tutorial series where we show you how to make a cool multi-directional tank battle game for the iPhone. In the first part of the series, we created a new Cocos2D 2.0 project with ARC support, added our tile map into the game, and added a tank we could move around with […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share

This is a multi-part tutorial series where we show you how to make a cool multi-directional tank battle game for the iPhone.

In the first part of the series, we created a new Cocos2D 2.0 project with ARC support, added our tile map into the game, and added a tank we could move around with the accelerometer.

In this second and final part of the tutorial, we will make our tank shoot bullets, add enemy tanks, add some win/lose logic, and add some polish and finishing touches.

This project begins where we left off last time, so grab the project where we left it off last time if you don’t have it already.

So, shall we make a game?

Running and Gunning

Right now our tank can move, but he can’t shoot! Since shooting is to tanks as buying shoes is to my lovely wife, we better fix this ASAP ;]

We already made it so that the tank moves with the accelerometer, so we’re all free to make the tank shoot wherever the user taps. However, to make the game more fun, instead of just shooting once when the user taps, we’ll make the tank shoot continously!

So let’s modify our Tank class to add some methods for shooting. Make the following changes to Tank.h:

// Add inside @interface
CGPoint _shootVector;
double _timeSinceLastShot;
CCSprite * _turret;

// Add after @interface
@property (assign) BOOL shooting;
- (void)shootToward:(CGPoint)targetPosition;
- (void)shootNow;

Here we add an instance variable to keep track of the direction we’re shooting in, and how long it’s been since the last shot. We also keep track of a new sprite we’ll be adding on top of the tank – the tank’s turret!

Next open Tank.m and make the following changes:

// Add to top of file
#import "SimpleAudioEngine.h"

// Add right after @implementation
@synthesize shooting = _shooting;

// Add inside initWithLayer:type:hp
NSString *turretName = [NSString stringWithFormat:@"tank%d_turret.png", type];
_turret = [CCSprite spriteWithSpriteFrameName:turretName];
_turret.anchorPoint = ccp(0.5, 0.25);
_turret.position = ccp(self.contentSize.width/2, self.contentSize.height/2);
[self addChild:_turret];

Firt we synthesize our variables. Next we create a new sprite for the tank’s turret, and add it as a child of the tank. This is so that when we move the tank sprite, the turret moves along with it.

Note how we position the turret:

  • We modify the anchor point to be pretty close to the base of the turret. Why? Because whatever the anchor point is set to is the point where rotation is around, and we want the turret to rotate around its base.
  • We then set the position of the sprite to be the center of the tank. Since this sprite is a child of the tank, it’s position is relative to the bottom left corner of the tank. So we’re “pinning” the anchor point (the base of the turret) to around the middle of the tank.

Next add a new method to the bottom of the file:

- (void)shootToward:(CGPoint)targetPosition {
    
    CGPoint offset = ccpSub(targetPosition, self.position);
    float MIN_OFFSET = 10;
    if (ccpLength(offset) < MIN_OFFSET) return;
    
    _shootVector = ccpNormalize(offset);

}

We'll call this method when the user taps a spot. We check to make sure the spot tapped is at least 10 points from the current position first (if it's too close, it's hard to tell where the user really wants to shoot). Then we normalize the vector (remember, this makes a vector of length one) so we have a vector in the direction we want to shoot in, and store that for future reference.

Add the method to actually shoot next:

- (void)shootNow {
    // 1
    CGFloat angle = ccpToAngle(_shootVector);
    _turret.rotation = (-1 * CC_RADIANS_TO_DEGREES(angle)) + 90;
    
    // 2
    float mapMax = MAX([_layer tileMapWidth], [_layer tileMapHeight]);
    CGPoint actualVector = ccpMult(_shootVector, mapMax);  
    
    // 3
    float POINTS_PER_SECOND = 300;
    float duration = mapMax / POINTS_PER_SECOND;
    
    // 4
    NSString * shootSound = [NSString stringWithFormat:@"tank%dShoot.wav", _type];
    [[SimpleAudioEngine sharedEngine] playEffect:shootSound];
    
    // 5
    NSString *bulletName = [NSString stringWithFormat:@"tank%d_bullet.png", _type];
    CCSprite * bullet = [CCSprite spriteWithSpriteFrameName:bulletName];
    bullet.tag = _type;
    bullet.position = ccpAdd(self.position, ccpMult(_shootVector, _turret.contentSize.height));        
    CCMoveBy * move = [CCMoveBy actionWithDuration:duration position:actualVector];
    CCCallBlockN * call = [CCCallBlockN actionWithBlock:^(CCNode *node) {
        [node removeFromParentAndCleanup:YES];
    }];
    [bullet runAction:[CCSequence actions:move, call, nil]];
    [_layer.batchNode addChild:bullet];
}

There's a bunch of code here so let's go over it bit by bit.

  1. First we rotate the turret to face in the direction we want to shoot. There's a handy function called ccpToAngle we can use that takes a vector and returns the angle of the vector in radians. We then convert this to degrees (which Cocos2D uses), multiply it by -1 because Cocos2D uses clockwise rotation. We also add on 90, which we need to do because our turret artwork is facing upwards (instead of to the right).
  2. Next we figure out how far we want to shoot the bullet. We basically want to shoot it a long way, so we get the max of the tile map's width or height and multiply it by the direction we want to shoot.
  3. Next we figure out how long it should take the bullet to get to that spot. This is simple - we divide the length of the vector (the max of the tile map's width or height) and divide it by the points per second we want the bullet to move.
  4. Gratuitous shooting sound effect! :]
  5. Finally we create a new bullet sprite, run an action on it to make it move (and disappear when it's done moving) and add it to the layer's batch node.

Next make these changes to Tank.m:

// Add new methods
- (BOOL)shouldShoot {
    
    if (!self.shooting) return NO;    
    
    double SECS_BETWEEN_SHOTS = 0.25;
    if (_timeSinceLastShot > SECS_BETWEEN_SHOTS) {        
        _timeSinceLastShot = 0;
        return YES;        
    } else {
        return NO;
    }
}

- (void)updateShoot:(ccTime)dt {
    
    _timeSinceLastShot += dt;
    if ([self shouldShoot]) {       
        [self shootNow];        
    }
    
}

// Add inside update
[self updateShoot:dt];

This is the code we need to make the tanks shoot continuosly. Every update loop we'll call updateShoot. This just checks if it's been at least 0.25 seconds since the last shot, and calls shootNow if so.

OK, finally done with Tank.m! Let's try this out. Open up HelloWorldLayer.m, and replace ccTouchesBegan and ccTouchesMoved with the following:

- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch * touch = [touches anyObject];
    CGPoint mapLocation = [_tileMap convertTouchToNodeSpace:touch];
    
    self.tank.shooting = YES;
    [self.tank shootToward:mapLocation];   
    
}

- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch * touch = [touches anyObject];
    CGPoint mapLocation = [_tileMap convertTouchToNodeSpace:touch];
    
    self.tank.shooting = YES;
    [self.tank shootToward:mapLocation];   
    
}

So basically, we're using taps to shoot now instead of move.

Build and run, and now you can tap the screen to unleash a hail of bullets! Mwuahaha, the power!

Tank shooting bullets in all directions

Note that the way we're shooting bullets here really isn't the best because we're continously allocating bullets, and allocations are expensive on iOS. A better way is to preallocate an array of bullets, and re-use old bullets from the array when you want to shoot one. We cover how to do this in the Space Game Starter Kit.

Contributors

Over 300 content creators. Join our team.