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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Adding Some Enemies

We can't have a tank game without enemies to shoot at, now can we?

So open up HelloWorldLayer.h and create an array for us to keep track of the enemy tanks in:

NSMutableArray * _enemyTanks;

Then open up HelloWorldLayer.m and add this code to the bottom of init to create a bunch of tanks:

_enemyTanks = [NSMutableArray array];
int NUM_ENEMY_TANKS = 50;
for (int i = 0; i < NUM_ENEMY_TANKS; ++i) {
    
    Tank * enemy = [[Tank alloc] initWithLayer:self type:2 hp:2];
    CGPoint randSpot;
    BOOL inWall = YES;
    
    while (inWall) {            
        randSpot.x = CCRANDOM_0_1() * [self tileMapWidth];
        randSpot.y = CCRANDOM_0_1() * [self tileMapHeight];
        inWall = [self isWallAtPosition:randSpot];                
    }
    
    enemy.position = randSpot;
    [_batchNode addChild:enemy];
    [_enemyTanks addObject:enemy];
    
}

This code should be pretty self explanitory. We create a bunch of tanks at random spots (as long as they aren't in walls).

Build and run, and you should see tanks all around the map! Since we were careful to allow our tank class to choose the artwork based on the type, they're differently colored too!

Enemy Tanks on the map

Shooting Enemies

Of course, they're just sitting around not doing anything, which is no fun! Let's add some basic logic to these tanks by subclassing the Tank class and overriding some of the methods. We'll call the class RandomTank, because our tank is going to move around more or less randomly.

So create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class RandomTank and make it a subclass of Tank. Open up RandomTank.h and replace it with the following:

#import "Tank.h"

@interface RandomTank : Tank {
    double _timeForNextShot;
}

@end

This adds a instance variable we keep track of how many seconds till we shoot next.

Move to RandomTank.m and replace it with the following:

#import "RandomTank.h"
#import "HelloWorldLayer.h"

@implementation RandomTank

- (id)initWithLayer:(HelloWorldLayer *)layer type:(int)type hp:(int)hp {
    
    if ((self = [super initWithLayer:layer type:type hp:hp])) {
        [self schedule:@selector(move:) interval:0.5];
    }
    return self;
    
}

- (BOOL)shouldShoot {
    
    if (ccpDistance(self.position, _layer.tank.position) > 600) return NO;
    
    if (_timeSinceLastShot > _timeForNextShot) {        
        _timeSinceLastShot = 0;
        _timeForNextShot = (CCRANDOM_0_1() * 3) + 1;
        [self shootToward:_layer.tank.position];
        return YES;
    } else {
        return NO;
    }
}

- (void)calcNextMove {
    
    // TODO  
    
}

- (void)move:(ccTime)dt {
    
    if (self.moving && arc4random() % 3 != 0) return;    
    [self calcNextMove];
    
}

@end

Here we schedule a move method to be called every half a second. Jumping down to the implementation, every time it calls it has a 1 in 3 chane to move the tank to another direction. We're gonna skip over implementing that right now and focus on the shooting though.

As far as the shooting goes, we first check to make sure the enemy tank is close enough to the hero tank. We don't want enemies far away shooting at our tank in this game, or it would be too hard.

We then figure out a random time for the next shot - somewhere between 1-4 seconds. If it's reached that time, we update the target to wherever the tank is and go ahead and shoot.

Let's try this out! Make the following chages in HelloWorldLayer.m:

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

// Modify the line in init to create a RandomTank instead of a normal tank
RandomTank * enemy = [[RandomTank alloc] initWithLayer:self type:2 hp:2];

That's it! Compile and run, and now the tanks will shoot at you when you draw near!

Shooting enemies

Moving Enemies

So far our enemies are shooting, but we haven't finished making them move.

To keep things simple for this game, the strategy we want to take is:

  1. Pick a nearby random spot.
  2. Make sure there's a clear path to that spot. If so, move toward it!
  3. Otherwise, return to step 1.

The only tricky thing is "making sure there's a clear path to that spot." Given a start and end tile coordinate, how can we walk through all of the tiles that the tank would move between and make sure they're all clear?

Luckily, this is a solved problem, and James McNeill has an excellent blog post on the matter. We'll just take his implementation and plug it in.

So go back to RandomTank.m and replace the calcNextMove method with the following:

// From http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html
- (BOOL)clearPathFromTileCoord:(CGPoint)start toTileCoord:(CGPoint)end
{
    int dx = abs(end.x - start.x);
    int dy = abs(end.y - start.y);
    int x = start.x;
    int y = start.y;
    int n = 1 + dx + dy;
    int x_inc = (end.x > start.x) ? 1 : -1;
    int y_inc = (end.y > start.y) ? 1 : -1;
    int error = dx - dy;
    dx *= 2;
    dy *= 2;
    
    for (; n > 0; --n)
    {
        if ([_layer isWallAtTileCoord:ccp(x, y)]) return FALSE;
        
        if (error > 0)
        {
            x += x_inc;
            error -= dy;
        }
        else
        {
            y += y_inc;
            error += dx;
        }
    }
    
    return TRUE;
}

- (void)calcNextMove {
    
    BOOL moveOK = NO;
    CGPoint start = [_layer tileCoordForPosition:self.position];
    CGPoint end;
    
    while (!moveOK) {
        
        end = start;
        end.x += CCRANDOM_MINUS1_1() * ((arc4random() % 10) + 3);
        end.y += CCRANDOM_MINUS1_1() * ((arc4random() % 10) + 3);
        
        moveOK = [self clearPathFromTileCoord:start toTileCoord:end];    
    }    
    
    CGPoint moveToward = [_layer positionForTileCoord:end];
    
    self.moving = YES;
    [self moveToward:moveToward];    
    
}

Don't worry about how the first method works (although you can read the blog post if you're curious) - just know it checks to see if there is a wall at any coordinate inbetween the start and end tile coordinates, and returns FALSE if so.

In calcNextMove, we follow our algorithm as described above. Pretty straigtforward eh?

That's it - build and run, and now you have moving enemies!

Collisions, Explosions, and Exits

Now that we have enemies to shoot at and bullets to dodge, I know what you guys want... tons of explosions, and a way to win the game!

Glad to oblige. Make the following changes to HelloWorldLayer.h:

// Add before @interface
typedef enum {
    kEndReasonWin,
    kEndReasonLose
} EndReason;

// Add inside @interface
CCParticleSystemQuad * _explosion;
CCParticleSystemQuad * _explosion2;
BOOL _gameOver;
CCSprite * _exit;

Next switch to HelloWorldLayer.m and add this to the bottom of init:

_explosion = [CCParticleSystemQuad particleWithFile:@"explosion.plist"];
[_explosion stopSystem];
[_tileMap addChild:_explosion z:1];

_explosion2 = [CCParticleSystemQuad particleWithFile:@"explosion2.plist"];
[_explosion2 stopSystem];
[_tileMap addChild:_explosion2 z:1];

_exit = [CCSprite spriteWithSpriteFrameName:@"exit.png"];
CGPoint exitTileCoord = ccp(98, 98);
CGPoint exitTilePos = [self positionForTileCoord:exitTileCoord];
_exit.position = exitTilePos;
[_batchNode addChild:_exit];

self.scale = 0.5;

Here we create the two types of explosions we'll be using and add them to the tile map, but make sure they're turned off. When we want to use them, we'll move them to where they should run and start them with resetSystem.

We also add an exit to the bottom right corner of the map. Once the tank reaches this spot, you win!

Finally note we set the scale of the layer to 0.5 because this game works a lot better when you can see more of the map at a time.

Next add these new methods right before update:

- (void)restartTapped:(id)sender {
    [[CCDirector sharedDirector] replaceScene:[CCTransitionZoomFlipX transitionWithDuration:0.5 scene:[HelloWorldLayer scene]]];   
}

- (void)endScene:(EndReason)endReason {
    
    if (_gameOver) return;
    _gameOver = true;
        
    CGSize winSize = [CCDirector sharedDirector].winSize;
    
    NSString *message;
    if (endReason == kEndReasonWin) {
        message = @"You win!";
    } else if (endReason == kEndReasonLose) {
        message = @"You lose!";
    }
    
    CCLabelBMFont *label;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        label = [CCLabelBMFont labelWithString:message fntFile:@"TanksFont.fnt"];
    } else {
        label = [CCLabelBMFont labelWithString:message fntFile:@"TanksFont.fnt"];
    }
    label.scale = 0.1;
    label.position = ccp(winSize.width/2, winSize.height * 0.7);
    [self addChild:label];
    
    CCLabelBMFont *restartLabel;
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"TanksFont.fnt"];    
    } else {
        restartLabel = [CCLabelBMFont labelWithString:@"Restart" fntFile:@"TanksFont.fnt"];    
    }
    
    CCMenuItemLabel *restartItem = [CCMenuItemLabel itemWithLabel:restartLabel target:self selector:@selector(restartTapped:)];
    restartItem.scale = 0.1;
    restartItem.position = ccp(winSize.width/2, winSize.height * 0.3);
    
    CCMenu *menu = [CCMenu menuWithItems:restartItem, nil];
    menu.position = CGPointZero;
    [self addChild:menu];
    
    [restartItem runAction:[CCScaleTo actionWithDuration:0.5 scale:4.0]];
    [label runAction:[CCScaleTo actionWithDuration:0.5 scale:4.0]];
    
}

These are my handy "game over" methods that I use very often when prototyping games. The most dedicated tutorial readers among you may remember this method from several other tutorials ;] Anyway we will use this to restart the game, and I won't cover it in detail here since it's pretty simple stuff.

Then add this code to the beginning of update:

// 1
if (_gameOver) return;

// 2
if (CGRectIntersectsRect(_exit.boundingBox, _tank.boundingBox)) {
    [self endScene:kEndReasonWin];
}

// 3
NSMutableArray * childrenToRemove = [NSMutableArray array];
// 4
for (CCSprite * sprite in self.batchNode.children) {
    // 5
    if (sprite.tag != 0) { // bullet     
        // 6       
        if ([self isWallAtPosition:sprite.position]) {
            [childrenToRemove addObject:sprite];
            continue;
        }
        // 7
        if (sprite.tag == 1) { // hero bullet
            for (int j = _enemyTanks.count - 1; j >= 0; j--) {
                Tank *enemy = [_enemyTanks objectAtIndex:j];
                if (CGRectIntersectsRect(sprite.boundingBox, enemy.boundingBox)) {
                    
                    [childrenToRemove addObject:sprite];
                    enemy.hp--;
                    if (enemy.hp <= 0) {
                        [[SimpleAudioEngine sharedEngine] playEffect:@"explode3.wav"];
                        _explosion.position = enemy.position;
                        [_explosion resetSystem];
                        [_enemyTanks removeObject:enemy];
                        [childrenToRemove addObject:enemy];
                    } else {
                        [[SimpleAudioEngine sharedEngine] playEffect:@"explode2.wav"];
                    }
                }
            }
        }
        // 8
        if (sprite.tag == 2) { // enemy bullet                
            if (CGRectIntersectsRect(sprite.boundingBox, self.tank.boundingBox)) {                    
                [childrenToRemove addObject:sprite];
                self.tank.hp--;
                
                if (self.tank.hp <= 0) {
                    [[SimpleAudioEngine sharedEngine] playEffect:@"explode2.wav"];                        
                    _explosion.position = self.tank.position;
                    [_explosion resetSystem];
                    [self endScene:kEndReasonLose];
                } else {
                    _explosion2.position = self.tank.position;
                    [_explosion2 resetSystem];
                    [[SimpleAudioEngine sharedEngine] playEffect:@"explode1.wav"];                        
                }
            }
        }
    }
}
for (CCSprite * child in childrenToRemove) {
    [child removeFromParentAndCleanup:YES];
}

This is our collision detection and game logic. Let's walk through it bit by bit.

  1. We're starting to keep track of whether the game is over, and there's no need to do any of this if the game is over. This is set when we call the endScene method we just added.
  2. If the tank intersects the exit, the player wins!
  3. We're about to start checking for collisions, and sometimes when things collide a sprite might be removed from the scene (for example, if a bullet intersects with a tank or a wall the bullet will be removed). However since we're iterating through the list of the children, we can't modify the list as we're iterating it. So we simply add what we want to remove to an array, and remove when we're all done.
  4. We set a tag on bullet sprites so we could identify them easy - equal to the type of the tank that fired it (1 or 2). We don't have a tag on anything else, so if there's a tag we know it's a bullet.
  5. If there's a wall where the bullet is, remove the bullet.
  6. If it's a bullet the hero shot, check to see if it hit any enamy tanks. If it does, reduce the enemy's HP (and destroy it if it's dead). Also play some gratuitous explosion sound effects and possibly an explosion particle system!
  7. Similarly, if it's an enemy bullet check to see if it's hit the player and react appropriately. Game over if the player reaches 0 HP.

As the last step, put this at the beginning of accelerometer:didAccelerate, ccTouchesBegan, and ccTouchesMoved:

if (_gameOver) return;

Build and run, and see if you can beat the game - and no fair hacking the code to win! ;]

You win!

Contributors

Over 300 content creators. Join our team.