How To Make a Tile-Based Game with Cocos2D 2.X

Charlie Fulton Charlie Fulton

This post is also available in: Japanese

Update 1/17/2013 Fully updated for Cocos2D 2.1 rc0, Tiled 0.9.0 (released Feb 2013), Xcode 4.6, and ARC. (original post by Ray Wenderlich, update by Charlie Fulton).

Mmm, tasty melons!

Mmm, tasty melons!

In this 2-part tutorial, we’re going to cover how to make a tile-based game with Cocos2D and the Tiled map editor. We will do so by creating a simple tile-based game where a ninja explores a desert in search of tasty watermelon things!

In this part of the tutorial, we’ll cover how to create a map with Tiled, how to add the map to the game, how to scroll the map to follow the player, and how to use object layers.

In the second part of the tutorial, we’ll cover how to make collidable areas in the map, how to use tile properties, how to make collectable items and modify the map dynamically, and how to make sure your ninja doesn’t overeat.

If you haven’t already, you may wish to start with the How To Make A Simple iPhone Game with Cocos2D 2.X Tutorial, since that covers most of the basics that we’ll build upon here.

Ok, so let’s have some fun with tile maps!

Creating a Project Skeleton

We’re going to start by creating a skeleton project to make sure we have all of the files we need for the rest of the project in the right spot.

Note: If you haven’t installed cocos2d 2.1 rc0 yet, download it here and then from a terminal run these commands:

cd ~/Downloads/cocos2d-iphone-2.1-rc0
./install-templates.sh -f -u

Create a new project in Xcode with the iOS / cocos2d v2.x / cocos2d iOS template, and name it TileGame. Select the options to make the project Universal and include git repository setup.

We are going to use ARC in this project, but by default the template isn’t set up to use ARC. Luckily, fixing it is really easy. Just go to Edit\Refactor\Convert to Objective-C ARC. Expand the dropdown and select only the last four files (main.m, AppDelegate.m, HelloWorldLayer.m, and IntroLayer.m), then click Check and finish the steps of the wizard.

Choose files to convert to ARC

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

Next, download this zip file of resources for the game. The zip file contains the following:

  • A sprite we’ll use for our player object. This may look familiar from the How to Make A Simple iPhone Game with Cocos2D Tutorial!
  • Some sound effects we’ll be using that I made with the excellent cxfr utility.
  • Some background music I made with Garage Band (see this post for more info).
  • The tile set we’ll be using – it actually comes with the map editor we’ll be using, but I thought it would be easier to include it with everything else.
  • Some additional “special” tiles we’ll explain a bit later.

Once you have the resources downloaded, unzip it and drag the TileGameResources folder to the “Resources” group in your project. Verify “Copy items into destination group’s folder (if needed)” is checked, “Create groups for any added folders” is selected, and click Finish.

If all works well, all of the files should be listed in your project.

Your project should look like this:

Project screenshot after resources and arc

That’s it for now – time to have some fun and make our map!

Making a Map with Tiled

Cocos2D supports maps created with the open source Tiled Map Editor and saved in TMX format.

If you visit the above link, you’ll see there are two versions of Tiled – one written with the Qt application framework, and one written with Java. There are two versions because Tiled was written first in Java, and they are porting it over to Qt.

Which version you use is largely up to you. In this tutorial we will cover using the Qt version because that is the development mainline for Tiled from now on, but some people like to use the Java version because not all of the old features have been completely ported over yet.

So anyway – if you would like to follow along, download the Qt version and install and run the app. Go to File\New, and fill in the dialog as follows:

New Map Dialog in Tile Map Editor

In the orientation section, you can choose between Orthogonal (think: the Legend of Zelda) or Isometric (think: Disgaea). We’re going to pick Orthogonal here.

Next you get to set up the map size. Keep in mind that this is in tiles, not pixels. We are going to make a smallish sized map, so choose 50×50 here. Tiled will show you the total map size in pixels, at the bottom of the new map dialog. This is calculated by multiplying the map size (50 tiles) by the tile size (32 px) in both height and width.

Finally you specify the tile width and height. What you choose here depends on the tile set that your artist will be making. For this tutorial, we are going to use some sample tiles that come with the Tiled editor which are 32×32, so choose that.

Next, we have to add the tile set that we’ll be using to draw our map. Click on “Map” in the menu bar, “New Tileset…”, and fill in the dialog as follows:

New Tileset Dialog in Tile Map Editor

To get the image, just click Browse and navigate to your TileGame/TileGameResources folder, and pick the tmw_desert_spacing.png file that you downloaded from the resource zip and added to your project. It will automatically fill out the name based on the filename.

You can leave the width and height as 32×32 since that is the size of the tiles. As for margin and spacing, I couldn’t find any good documentation on the exact meaning of these, but this is what I think they mean:

  • Margin is how many pixels Tiled should skip (for both width and height) for the current tile before it starts looking for actual tile pixels.
  • Spacing is how many pixels Tiled should advance (for both width and height) after it reads the actual tile pixels to get to the next tile data.

If you take a look at tmw_desert_spacing.png, you’ll see that each tile has a 1px black border around it, which is would explain the settings of margin and spacing as 1.

Zoomed screenshot of desert tiles

Once you click OK, you will see the tiles show up in the Tilesets window. Now you can start drawing away! Simply click the “Stamp” icon in the toolbar, then click a tile, then click anywhere on the map you’d like to place a tile.

Using the Stamp Tool in Tiled

So go ahead and draw yourself a map – be as creative as you’d like! Make sure to add at least a couple buildings on the map, because we’ll need something to collide into later!

Drawing Our Map in Tiled

Some handy shortcuts to keep in mind:

  • You can drag a box around a series of tiles in the Tileset picker, to put down multiple adjacent tiles at the same time.
  • You can use the paint button in the toolbar to paint the entire background with a base tile.
  • You can zoom in and out with “View\Zoom In…” and “View\Zoom Out…”.
  • The “z” key will now rotate the tile when editing a map with the stamp tool

There is a new feature you might have noticed “Mini-map”. This is an awesome feature, it let’s you see a (you guessed it) Mini-map! Take a look at my poor attempt at a maze in the bottom of the mini-map below. The red box indicates the area you see in the main editing window.

New feature mini-map

Keep this mini-map view in mind when we talk about scrolling in the next section.

Once you’re done drawing the map, double click on the Layer in Layers (which probably says “Tile Layer 1″ right now), and change the name to “Background”. Then click “File\Save” and save the file to TileGame\TileGameResources in the TileGame project, and name the file “TileMap.tmx”.

We’re going to do some more stuff with Tiled later, but for now let’s get this map into our game!

Adding the Tiled Map to our Cocos2D Scene

First thing first, right click on the TileGameResources group, click “Add\Existing Files…” and add the new TileMap.tmx file you just created to your project.

Next let’s turn off Retina Display support because we don’t have any retina sized art.

Make the following changes to AppDelegate.m:

 
// comment out these lines in the -application:didFinishLaunchingWithOptions: method
// this should be lines 93-94
 
//	if( ! [director_ enableRetinaDisplay:YES] )
//		CCLOG(@"Retina Display Not supported");

Open up HelloWorldLayer.h, and replace the contents with the following:

#import <GameKit/GameKit.h>
 
// When you import this file, you import all the cocos2d classes
#import "cocos2d.h"
 
// HelloWorldLayer
@interface HelloWorldLayer : CCLayer
{
}
 
// returns a CCScene that contains the HelloWorldLayer as the only child
+(CCScene *) scene;
 
@end

Replace the contents of HelloWorldLayer.m with the following:

#import "HelloWorldLayer.h"
#import "AppDelegate.h"
 
@interface HelloWorldLayer()
 
@property (strong) CCTMXTiledMap *tileMap;
@property (strong) CCTMXLayer *background;
 
@end
 
@implementation HelloWorldLayer
 
// Helper class method that creates a Scene with the HelloWorldLayer as the only child.
+(CCScene *) scene
{
	// 'scene' is an autorelease object.
	CCScene *scene = [CCScene node];
 
	// 'layer' is an autorelease object.
	HelloWorldLayer *layer = [HelloWorldLayer node];
 
	// add layer as a child to scene
	[scene addChild: layer];
 
	// return the scene
	return scene;
}
 
-(id) init
{
	if( (self=[super init]) ) {
 
        self.tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"TileMap.tmx"];
        self.background = [_tileMap layerNamed:@"Background"];
 
        [self addChild:_tileMap z:-1];
    }
    return self;
}
@end

Here we make a call to the CCTMXTiledMap file, instructing it to create a map from the map file we created with Tiled.

Some quick background on CCTMXTiledMap. It’s a CCNode, so you can set its position, scale, etc. The children of the node are the layers of the map, and there’s a helper function where you can look the up by name – which we do here to get the background. Each layer is a subclass of CCSpriteSheet for performance reasons – but this also means that you can only have one tileset per layer.

So all we do here is save a reference to the tile map and the background layer, then add the tile map to the HelloWorld layer.

And that’s it! Compile and run the code, and you should see the bottom left corner of your map:

Screenshot of the Tile Map Edge on our iPhone

Not bad! But for this to be a game, we need three things: a) a player, b) a starting point to put the player, and c) to move our view so that we are looking at the player.

And this is where it gets tricky. So let’s tackle this next!

Tiled Object Layers and Setting Tile Map Position

Tiled supports two kinds of layers – tile layers (which is what we’ve been working with so far), and object layers.

Object layers allow you to draw boxes around portions of the maps to specify areas where things might happen. For example, you might make an area where monsters spawn, or an area that is deadly to enter. In our case, we’re going to create an area for our “spawn point” for our player.

So go to the menu bar in Tiled and pick “Layer\Add Object Layer…”, name the layer “Objects”, and click OK. To insert an Object, select the Insert Rectangle(R) item from the toolbar. Your current layer is shown in the bottom left corner.

If you draw on the map, you’ll notice it doesn’t draw a tile, instead it draws a rectangle, which you can expand to cover multiple tiles or move around. In the latest version of tiled you can also draw other types of object shapes.

We just want to select one tile for the player to start in. So choose somewhere on your map and click the tile. The size of the box doesn’t really matter, since we’ll just be using the x, y coordinates.

Tile Map with Object

Then right click the gray object you just added, and click “Object Properties”. Give it a name of “SpawnPoint” and click OK:

Tiled Object Properties

Supposedly, you can get fancy here and set the Type of the object to a Cocos2D class name and it will create an object of that type for you (such as CCSprite), but I couldn’t find where it was doing that in the source code. Update: Tyler from GeekAndDad.com pointed out that the code used to be in a previous version of Cocos2D, but was removed due to issues with it a while back.

Anyway – we’re just going to leave the type blank, which will create an NSMutableDictionary for us where we can access the various aspects of the object, including the x, y coordinates.

Save the map and go back to Xcode. Make the following changes to HelloWorldLayer.m:

// After the interface declaration, with the other private properties
@property (strong) CCSprite *player;
 
// Inside the init method, after setting self.background
CCTMXObjectGroup *objectGroup = [_tileMap objectGroupNamed:@"Objects"];
NSAssert(objectGroup != nil, @"tile map has no objects object layer");
 
NSDictionary *spawnPoint = [objectGroup objectNamed:@"SpawnPoint"];
int x = [spawnPoint[@"x"] integerValue];
int y = [spawnPoint[@"y"] integerValue];
 
_player = [CCSprite spriteWithFile:@"Player.png"];
_player.position = ccp(x,y);
 
[self addChild:_player];
[self setViewPointCenter:_player.position];

Ok let’s stop for a second and explain the bit about the object layer and object groups. First note that you retrieve object layers via the objectGroupNamed method on the CCTMXTiledMap object (rather than layerNamed). It returns a special CCTMXObjectGroup object.

We then call the objectNamed method on the CCTMXObjectGroup to get a NSMutableDictionary containing a bunch of useful info about the object, including x and y coordinates, width, and height. In this case all we care about is the x,y coordinates, so we pull those out and set that as the position of our player sprite.

At the end we want to set the view to focus on where the player is. So now add the following new method to HelloWorldLayer.m (anywhere in the file is fine now in Objective-C):

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

Ok, let’s explain this a bit too. Imagine this function is setting the center of a camera. We allow the user to pass in any x,y coordinate in the map here – but if you think about it there are some points that we don’t want to be able to show – for example we don’t want the screen to extend beyond the edges of the map (where it would just be blank space!)

For example, take a look at this diagram:

Diagram of tile map vs. viewport in Cocos2D

See how if the center of the camera is less than winSize.width/2 or winSize.height/2, part of the view would be off the screen? Similarly, we need to check the upper bounds as well, and that’s exactly waht we do here.

Now so far we’ve been treating this function as if it was setting the center of where a camera was looking. However… that isn’t exactly what we’re doing. There is a way in Cocos2D to manipulate the camera of a CCNode, but using that can make things more difficult than the solution we’re going to use: moving the entire layer instead.

Take a look at this diagram:

Diagram of how to move layer to fit within view in Cocos2D

Imagine a big world, and we’re looking at the coordinates from 0 to winSize.height/width. The center of our view is centerOfView, and we know where we want the center to be (actualPosition). So to get the actual position to match up to the center of view, all we do is slide the map down to match!

This is accomplished by subtracting the actual position from the center of view, and then setting the HelloWorld layer to that position.

Phew! Enough theory – let’s see it in action! Compile and run the project, and if all goes well you should see your ninja in the scene, with the view moved to show him strutting his stuff!

Screenshot of our scene centered on our Ninja

Making the Ninja Move

We’re off to a good start, but our ninja is just sitting there! And that’s not very ninja-like.

Let’s make the ninja move simply by moving him in the direction the user taps. Add the following code to HelloWorldLayer.m:

// Inside init method
self.touchEnabled = YES;
 
#pragma mark - handle touches
-(void)registerWithTouchDispatcher
{
    [[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self
                                                              priority:0
                                                       swallowsTouches:YES];
}
 
-(BOOL) ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
	return YES;
}
 
-(void)setPlayerPosition:(CGPoint)position {
	_player.position = position;
}
 
-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
    CGPoint touchLocation = [touch locationInView:touch.view];
    touchLocation = [[CCDirector sharedDirector] convertToGL:touchLocation];
    touchLocation = [self convertToNodeSpace:touchLocation];
 
    CGPoint playerPos = _player.position;
    CGPoint diff = ccpSub(touchLocation, playerPos);
 
    if ( abs(diff.x) > abs(diff.y) ) {
        if (diff.x > 0) {
            playerPos.x += _tileMap.tileSize.width;
        } else {
            playerPos.x -= _tileMap.tileSize.width;
        }        
    } else {        
        if (diff.y > 0) {
            playerPos.y += _tileMap.tileSize.height;
        } else {
            playerPos.y -= _tileMap.tileSize.height;
        }        
    }
 
    CCLOG(@"playerPos %@",CGPointCreateDictionaryRepresentation(playerPos));
 
    // safety check on the bounds of the map
    if (playerPos.x <= (_tileMap.mapSize.width * _tileMap.tileSize.width) &&
        playerPos.y <= (_tileMap.mapSize.height * _tileMap.tileSize.height) &&
        playerPos.y >= 0 &&
        playerPos.x >= 0 )
    {
        [self setPlayerPosition:playerPos];
    }
 
    [self setViewPointCenter:_player.position];
}

First, we set our layer as touch enabled in the init method. Then we override the registerWithTouchDispatcher method to register ourselves to handle targed touch events. This will result in ccTouchBegan/ccTouchEnded methods being called (singular case), instead of ccTouchesBegan/ccTouchesEnded methods (plural case).

You may wonder why even bother with this, since we used the ccTouchesBegan/ccTouchesEnded method just fine in the How To Make A Simple iPhone Game with Cocos2D 2.X Tutorial. It’s true, in this case it doesn’t matter either way. However, I wanted to introduce everyone to this method in case you hadn’t seen it already, because it has two significant advantages (these are listed verbatim from the cocos2D source):

  • “You don’t need to deal with NSSets, the dispatcher does the job of splitting them. You get exactly one UITouch per call.”
  • “You can *claim* a UITouch by returning YES in ccTouchBegan. Updates of claimed touches are sent only to the delegate(s) that claimed them. So if you get a move/ended/cancelled update you’re sure it’s your touch. This frees you from doing a lot of checks when doing multi-touch.”

Anyway, inside our ccTouchEnded location, we convert the location to view coordinates and then to GL coordinates as usual. What is new is we call [self convertToNodeSpace:touchLocation].

This is because the touch location will give us coordinates for where the user tapped inside the viewport (for example 100,100). But we might have scrolled the map a good bit so that it actually matches up to (800,800) for example. So calling this method offsets the touch based on how we have moved the layer.

Next, we figure out the difference between the touch and the player position. We have to choose a direction based on the touch, so first we decide whether to move up/down or side to side based on whichever is the greatest distance away. Then we just see if it’s positive or negative to move up or down.

We adjust the player position accordingly, and then set the viewpoint center to be the player position, which we already wrote in the last section!

Update: Note we have to add a safety check to make sure we’re not moving our player off the map as well! This was pointed out by Geek & Dad from the comments section – thanks!

So compile and run the project and try it out! You should now be able to tap the screen to move the ninja around.

Our Ninja can now move around the map!

Where To Go From Here?

That’s all for this part of the tutorial. At this point you should know the basics about creating maps and importing them into your game.

Here’s a sample project with the code we’ve developed so far.

Next check out part 2 of the tutorial, where we show how to add collision detection into the map, to prevent our ninja from happily walking through the walls!

Charlie Fulton
Charlie Fulton

Charlie Fulton is a full time iOS developer. He has worked with many languages and technologies in the past 16 years, and is currently specializing in iOS and Cocos2D development. In his spare time, Charlie enjoys hunting, fishing, and hanging out with his family.

You can follow Charlie on Twitter.

User Comments

31 Comments

[ 1 , 2 , 3 ]
[ 1 , 2 , 3 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Matt Luedke

... 49 total!

Update Team

  • Zouhair Mahieddine

Editorial Team

  • John Clem

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Jose De La Roca

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!