How To Make A Multi-Directional Scrolling Shooter – Part 1

A while back in the weekly tutorial vote, you guys said you wanted a tutorial on how to make a multi-directional scrolling shooter. Your wish is my command! :] In this tutorial series, we’ll make a tile-based game where you drive a tank around using the accelerometer. Your goal is to get to the exit, […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share

A while back in the weekly tutorial vote, you guys said you wanted a tutorial on how to make a multi-directional scrolling shooter. Your wish is my command! :]

In this tutorial series, we’ll make a tile-based game where you drive a tank around using the accelerometer. Your goal is to get to the exit, without being blasted by enemy tanks!

To see what we’ll make, check out this video:

In this first part of the series, you’ll get some hands on experience with Cocos2D 2.X, porting it to use ARC, using Cocos2D vector math functions, working with tile maps, and much more!

This tutorial assumes you have some basic knowledge of Cocos2D. If you are new to Cocos2D, you may wish to check out some of the other Cocos2D tutorials on this site first. In particular, you should review the tile-based game tutorial before this tutorial.

Rev up your coding engines, and let’s begin!

Getting Started

We’re going to use Cocos2D 2.X in this project, so go ahead and download it if you don’t have it already.

Double click the tar to unarchive it, then install the templates with the following commands:

cd ~/Downloads/cocos2d-iphone-2.0-beta
./install-templates.sh -f -u

Next create a new project in Xcode with the iOS/cocos2d/cocos2d template, and name it Tanks.

We want to use ARC in this project to make memory management simpler, but by default the template isn’t set up to use ARC. So let’s fix that by performing the following 5 steps:

  1. Control-click the libs folder in your Xcode project and click Delete. Then click Delete again to delete the files permanently. This removes the Cocos2D files from our project – but that’s OK, because we will link in the project separately in a minute. We are doing this so we can set up our project to use ARC (but allow the Cocos2D code to be non-ARC).
  2. Find where you downloaded Cocos2D 2.0 to, and find the cocos2d-ios.xcodeproj inside. Drag that into your project.
  3. Click on your project, select the Tanks target, and go to the Build Phases tab. Expand the Link Binary With Libraries section, click the + button, select libcocos2d.a and libCocosDenhion.a from the list, and click add.

Linking Cocos2D libs

  1. Click the Build Settings tab and scroll down to the Search Paths section. Set Always Search User Paths to YES, double click User Header Search Paths, and enter in the path to the directory where you’re storing Cocos2D 2.0. Make sure Recursive is checked.

Adding Cocos2D User Header Paths

  1. From the main menu go to Edit\Refactor\Convert to Objective-C ARC. Select all of the files from the dropdown and go through the wizard. It should find no problems, so just finish up the conversion.

And that’s it! Build and run and make sure everything still works OK – you should see the normal Hello World screen.

But you might notice that it’s in portrait mode. We want landscape mode for our game, so open RootViewController.m and make sure shouldAutorotateToInterfaceOrientation looks like the following:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) );
}

Build and run and now we have a landscape game with the latest and greatest version of Cocos2D 2.0, ARC compatibile. w00t!

Adding the Resources

First things first – download the resources for this project and drag the two folders inside (Art and Sounds) into your project. Make sure “Copy items into destination group’s folder” is checked, and “Create groups for any added folders” is selected, and click Finish.

Here’s what’s inside:

  • Two particle effects I made with Particle Designer – two different types of explosions.
  • Two sprite sheets I made with Texture Packer. One contains the background tiles, and one contains the foreground sprites.
  • A font I made with Glyph Designer that we’ll use in the HUD and game over menu.
  • Some background music I made with Garage Band.
  • Some sound effects I made with cxfr.
  • The tile map itself, which I made with Tiled.

The most important thing here is obviously the tile map. I recommend you download Tiled if you don’t have it already, and use it to open up tanks.tmx to take a look.

A tile map made with Tiled

As you can see, it’s a pretty simple map with just three types of tiles – water, grass, and wood (for bridges). If you right click on the water tile and click Properties, you’ll see that it has a property for “Wall” defined, which we’ll be referring to in code later:

A tile map property defined with Tiled

There’s just one layer (named “Background”), and we don’t add anything onto the map for the sprites like the tanks or the exit – we’ll add those in code.

Feel free to modify this map to your desire! For more info on using Tiled, see our earlier tile-based game tutorial.

Adding the Tile Map and Helpers

Next let’s add the tile map to our scene. As you know, this is ridiculously easy in Cocos2D.

Open HelloWorldLayer.h and add two instance variables into HelloWorldLayer:

CCTMXTiledMap * _tileMap;
CCTMXLayer * _bgLayer;

We’re keeping track of the tile map and the one and only layer inside (the background layer) in these variables, because we’ll need to refer to them often.

Then open HelloWorldLayer.m and replace the init method with the following:

-(id) init
{
    if( (self=[super init])) {

        _tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"tanks.tmx"];
        [self addChild:_tileMap];

        _bgLayer = [_tileMap layerNamed:@"Background"];

    }
    return self;
}

Here we just create the tile map, add it to the layer, and get a reference to the background layer in the tile map.

Build and run, and you’ll see the bottom left corner of the map:

Bottom left corner of the map

In this game we want to start our tank in the upper left corner. To make this easy, let’s build up a series of helper methods. I use these helper methods in almost any tile-based game app I work on, so you might find these handy to use in your own projects as well.

First, we need some methods to get the height and width of the tile map in points. Add these in HelloWorldLayer.m, above init:

- (float)tileMapHeight {
    return _tileMap.mapSize.height * _tileMap.tileSize.height;
}

- (float)tileMapWidth {
    return _tileMap.mapSize.width * _tileMap.tileSize.width;
}

The mapSize property on a tile map returns the size in number of tiles (not points) so we have to multiply the result by the tileSize to get the size in points.

Next, we need some methods to check if a given position is within the tile map – and likewise for tile coordinate.

In case you forgot what a tile coordinate is, each tile in the map has a coordinate, starting with (0,0) for the upper left and (99,99) for the bottom right (in our case). Here’s a screenshot from the earlier tile-based game tutorial:

Tile Coordinates in Tiled

So add these methods that will verify positions/tile coordinates right after the tileMapWidth method:

- (BOOL)isValidPosition:(CGPoint)position {
    if (position.x < 0 ||
        position.y < 0 ||
        position.x > [self tileMapWidth] ||
        position.y > [self tileMapHeight]) {
        return FALSE;
    } else {
        return TRUE;
    }
}

- (BOOL)isValidTileCoord:(CGPoint)tileCoord {
    if (tileCoord.x < 0 || 
        tileCoord.y < 0 || 
        tileCoord.x >= _tileMap.mapSize.width ||
        tileCoord.y >= _tileMap.mapSize.height) {
        return FALSE;
    } else {
        return TRUE;
    }
}

These should be pretty self-explanitory. Obviously negative positions/coordinates would be outside of the map, and the upper bound is the width/height of the map, in points or tiles respectively.

Next, add methods to convert between positions and tile coordinates:

- (CGPoint)tileCoordForPosition:(CGPoint)position {    
    
    if (![self isValidPosition:position]) return ccp(-1,-1);
    
    int x = position.x / _tileMap.tileSize.width;
    int y = ([self tileMapHeight] - position.y) / _tileMap.tileSize.height;
    
    return ccp(x, y);
}

- (CGPoint)positionForTileCoord:(CGPoint)tileCoord {
    
    int x = (tileCoord.x * _tileMap.tileSize.width) + _tileMap.tileSize.width/2;
    int y = [self tileMapHeight] - (tileCoord.y * _tileMap.tileSize.height) - _tileMap.tileSize.height/2;
    return ccp(x, y);
    
}

The first method converts from a position to a tile coordinate. Converting the x coordinate is easy – it just divides the number of points by the points per tile (discarding the fraction) to get the tile number it’s inside. The y coordinate is similar, except it first has to subtract the y value from the tile map height to “flip” the y value, because positions have 0 at the bottom, but tile coordinates have 0 at the top.

The second method does the oppostie – tile coordinate to position. This is pretty much the same idea, but notice that there are a lot of potential points inside a tile that this method could return. We choose to return the center of the tile here, because that works nicely with Cocos2D since you often want to place a sprite at the center of a tile.

Now that we have this handy library built up, we can now build a routine to allow scrolling the map to center something (namely our tank) within the view. Add this next:

-(void)setViewpointCenter:(CGPoint) position {
    
    CGSize winSize = [[CCDirector sharedDirector] winSize];
    
    int x = MAX(position.x, winSize.width / 2 / self.scale);
    int y = MAX(position.y, winSize.height / 2 / self.scale);
    x = MIN(x, [self tileMapWidth] - winSize.width / 2 / self.scale);
    y = MIN(y, [self tileMapHeight] - winSize.height/ 2 / self.scale);
    CGPoint actualPosition = ccp(x, y);
    
    CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
    CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
    
    _tileMap.position = viewPoint;
    
}

The easiest way to explain this is through a picture:

Tile Map Scrolling

To make a given point centered, we move the tile map itself. If we subtract our “goal” position from the center of the view, we’ll get the “error” and we can move the map that amount.

The only tricky part is there are certain points we shouldn’t be able to set in the center. If we try to center the map on a position less than half the window size, then empty “black” space would be visible to the user, which isn’t very nice. Same thing for if we try to center a position on the very top of the map. So these checks take care of that.

Now that we have the helper methods in place, let’s try it out! Add the following inside the init method:

CGPoint spawnTileCoord = ccp(4,4);
CGPoint spawnPos = [self positionForTileCoord:spawnTileCoord];
[self setViewpointCenter:spawnPos];

Build and run, and now you’ll see the upper left of the map – where we’re about to spawn our tank!

Upper left of the map

Contributors

Over 300 content creators. Join our team.