Collisions and Collectables: How To Make a Tile-Based Game with Cocos2D 2.X Part 2

Charlie Fulton Charlie Fulton

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, that was tasty!

Mmm, that was tasty!

This is the second part of a 2-part tutorial where we cover how to make a tile-based game with Cocos2D and the Tiled map editor. We are creating a simple tile-based game where a ninja explores a desert in search of tasty watermelon-things!

In the first part of the tutorial, we covered 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 this 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.

So let’s pick up where we left off last time and make our map a bit more game-like!

Tiled Maps and Collisions

You may have noticed that currently the ninja can move right through walls and obstacles with no problem at all. He is a ninja, but even ninjas aren’t that good!

So we need to figure out a way to mark some tiles as “collidable” so we can prevent the player from moving into those positions. There are many possible solutions to this (including using object layers), but I’m going to show you a new technique that I think is effective and also a good learning exercise – using a meta layer and layer properties.

Let’s dive right in! Load up Tiled again, click “Layer\Add Tile Layer…”, name the Layer “Meta”, and click OK. This is the layer we will be putting a few fake tiles in to indicate “special tiles”.

So now we need to add our special tiles. Click “Map\New Tileset…”, browse to meta_tiles.jpg in your TileGame\TileGameResources folder, and click Open. Set the Margin and Spacing to 1 and click OK.

With the layers window selected, click on meta_tiles in the Tilesets window. You will see two tiles: red and green.

Tiled and Meta Tiles

There is nothing at all special about these tiles – I just made a simple image with two red and green tiles with partial transparency. However we’re going to decide that red means “collidable” (we’ll use green later on), and paint our scene appropriately.

So make sure the Meta layer is selected, choose the stamp tool, choose the red tile, and paint over any object that you want the ninja to collide with. When you’re done it might look like the following:

Tiled and Painting Tiles for Collision Detection

Next, we need to set a property on the tile to flag it so we can recognize in code that this is the tile that is “collidable.” Right click on the red tile in the Tilesets section, and click “Tile Properties…”. Add a new property for “Collidable” set to “True” like the following:

Tile Properties in Tiled

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

// Inside the HelloWorldLayer interface with the other private properties
@property (strong) CCTMXLayer *meta;
 
// In init, right after loading background
self.meta = [_tileMap layerNamed:@"Meta"];
_meta.visible = NO;
 
// Add new method
- (CGPoint)tileCoordForPosition:(CGPoint)position {
    int x = position.x / _tileMap.tileSize.width;
    int y = ((_tileMap.mapSize.height * _tileMap.tileSize.height) - position.y) / _tileMap.tileSize.height;
    return ccp(x, y);
}

Ok let’s stop here a second. We declare a member/variable for the meta layer as usual, and load a reference from the tile map. Note that we mark the layer as invisible since we don’t want to see these objects, they are for annotating what is collidable only.

Next we add a new helper method that helps us convert x,y coordinates to “tile coordinates”. Each of the tiles has a coordinate, starting with (0,0) for the upper left and (49,49) for the bottom right (in our case).

Tile Coordinates in Tiled

The above screenshot is from the Java version of Tiled, btw. Showing the coordinates for tiles is a feature I don’t think they’ve ported to the Qt version yet.

Anyway, some of the functions we’re about to use require tile coordinates rather than x,y coordintes, so we need a way to convert the x,y coordinates into tile coordinates. This is exactly what this function does!

Getting the x coordinate is easy – we just divide it by the width of a tile. To get the y coordinate, we have to flip things around because in Cocos2D (0,0) is at the bottom left, not the top left.

Next replace the contents of setPlayerPosition with the following:

CGPoint tileCoord = [self tileCoordForPosition:position];
int tileGid = [_meta tileGIDAt:tileCoord];
if (tileGid) {    
    NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
    if (properties) {        
        NSString *collision = properties[@"Collidable"];
        if (collision && [collision isEqualToString:@"True"]) {            
            return;
        }
    }
}
_player.position = position;

Here we convert the x,y coordinates for the player to tile coordinates. Then we use the tileGIDAt function in the meta layer to get the GID at the specified tile coordinate.

Huh, what’s a GID? GID stands for “globally unique identifier” (I think). But in this case I like to think of it as the id for the tile we’re using, which would be the red square if that’s where we’re trying to move.

We then use the GID to look up properties for that tile. It returns a dictionary of properties, so we look through to see if “Collidable” is set to true, and if it is we return immediately, hence not setting the player position and making the move invalid.

And that’s it! Compile and run the project, and you should now no longer be able to walk through any tiles you painted red:

Screenshot demonstrating you cannot move through walls

Modifying the Tiled Map Dynamically

So far our ninja is having a fine time exploring, but this world is a little dull. There’s simply nothing to do!

Plus our ninja looks a bit hungry. So let’s spice things up by giving our ninja something to eat.

For this to work, we’re going to have to create a foreground layer for any objects we want the user to collect. That way, we can simply delete the tile from the foreground layer when the ninja picks it up, and the background will show through.

So open up Tiled, go to “Layer\Add Tile Layer…”, name the layer “Foreground”, and click OK. Make sure the Foreground layer is selected, and add a couple collectibles to your map. I liked to use the tile that looks like a Watermelon or something to me.

Adding collectibles to the Tile Map

Now, we need to mark those tiles as collectible, similarly to how we marked some of the tiles as collidable. Select the meta layer, switch over to the meta_tiles, and paint a green tile over each of your collectables. You’ll have to click “Layer\Raise Layer” to make sure the meta layer is on top so that the green is visible.

Painting collectables with meta tiles

Next, we need to add the property to the tile to mark it as collectable. Right click on the green tile in the Tilesets section, click “Tile Properties…” and add a new property with name “Collectable”, value “True”.

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

// in HelloWorldLayer interface declaration with the other private properties
@property (strong) CCTMXLayer *foreground;
 
// In init, right after loading background
self.foreground = [_tileMap layerNamed:@"Foreground"];
 
// Add to setPlayerPosition, right after the if clause with the return in it
NSString *collectible = properties[@"Collectable"];
if (collectible && [collectible isEqualToString:@"True"]) {
    [_meta removeTileAt:tileCoord];
    [_foreground removeTileAt:tileCoord];    
}

Here is standard stuff to keep a reference to the foreground layer. The new thing is we check to see if the tile the player is moving to has the “Collectable” property. If it does, we use the removeTileAt method to remove the tile from both the meta layer and the foreground layer.

Compile and run the project, and now your ninja will be able to dine on tasty-melon-thingie!

Screenshot of Ninja about to eat a melon

Creating a Score Counter

Our ninja is happy and fed, but as players we’d like to know how many melons he’s eaten. You know, we don’t want him getting fat on us.

Usually we’d just add a label to our layer and be done with it. But wait a minute – we’re moving the entire layer all the time, that will screw us up, oh noes!

This is a good opportunity to show how to use multiple layers in a scene – this is the type of situation they are built for. We’ll keep our HelloWorld layer as we’ve been doing, but we’ll make an additional Layer called HelloWorldHud to display our label. (Hud means heads up display).

Of course, our two layers need some method of communicating – the Hud layer will want to know when the ninja snacks on a melon. There are many many ways of getting the two layers to communicate, but we’ll go with the most simple way possible – we’ll hand the HelloWorld layer a pointer to the HelloWorldHud layer, and it can call a method to notify it when the ninja snacks.

So add the following to HelloWorldLayer.h:

// Before HelloWorldLayer class declaration after #import "cocos2d.h"
@interface HudLayer : CCLayer
- (void)numCollectedChanged:(int)numCollected;
@end

And make the following changes to HelloWorldLayer.m:

// At top of file after imports (from @implementation -> @end)
@implementation HudLayer 
{
    CCLabelTTF *_label;
}
 
- (id)init 
{
    self = [super init];
    if (self) {
        CGSize winSize = [[CCDirector sharedDirector] winSize];
        _label = [CCLabelTTF labelWithString:@"0" fontName:@"Verdana-Bold" fontSize:18.0];
        _label.color = ccc3(0,0,0);
        int margin = 10;
        _label.position = ccp(winSize.width - (_label.contentSize.width/2) - margin, _label.contentSize.height/2 + margin);
        [self addChild:_label];
    }
    return self;
}
 
-(void)numCollectedChanged:(int)numCollected 
{    
    _label.string = [NSString stringWithFormat:@"%d",numCollected];
}
@end
 
 
// Inside the HelloWorldLayer interface with the other private properties
@property (strong) HudLayer *hud;
@property (assign) int numCollected;
 
 
// Add to the +(CCScene *) scene method right before the return
HudLayer *hud = [HudLayer node];
[scene addChild:hud];
layer.hud = hud;
 
// Add inside setPlayerPosition, in the case where a tile is collectable
self.numCollected++;
[_hud numCollectedChanged:_numCollected];

Nothing too fancy here. Our second layer derives from CCLayer and just adds a simple label to the bottom right corner. We modify the scene to add the second layer to the scene as well, and pass the HelloWorld layer a reference to the Hud. Then we modify the HelloWorldLayer to call a method on the Hud when the count changes, so it can update the label accordingly.

Compile and run the project, and if all goes well you should see a melon counter in the bottom right!

Screenshot of melon counter

Gratuituous Sound Effects and Music

You know this wouldn’t be a game tutorial from this site without completely unnecessary but fun sound effects and music :]

Simply make the following changes to HelloWorldLayer.m:

// At top of file
#import "SimpleAudioEngine.h"
 
// At top of init for HelloWorldLayer
[[SimpleAudioEngine sharedEngine] preloadEffect:@"pickup.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"hit.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"move.caf"];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"TileMap.caf"];
 
// In case for collidable tile
[[SimpleAudioEngine sharedEngine] playEffect:@"hit.caf"];
 
// In case of collectable tile
[[SimpleAudioEngine sharedEngine] playEffect:@"pickup.caf"];
 
// Right before setting player position
[[SimpleAudioEngine sharedEngine] playEffect:@"move.caf"];

Now our ninja can groove happy as he eats!

Where To Go From Here?

That’s it for this tutorial series, at least for now. You should have a good grasp on the most important concepts related to using tile maps in Cocos2D at this point.

Here is a copy of the Tile-Based Cocos2D game that we’ve developed so far.

If you enjoyed this series, our good friends from Geek and Dad have developed a follow-up to the series (not updated for Cocos2D 2.X yet though): Enemies and Combat: How To Make a Tile-Based Game with Cocos2D Part 3! Check it out to see how you can extend the game to add enemies, projectiles, and a win/lose scene!

If you have any additional tips or suggestions for how to effectively use Tiled or tile-based maps in Cocos2D effectively, or if you’ve used or are planning to use tile-based maps in your projects, please share below!

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

13 Comments

  • Brilliant tutorials, clear and easy to follow thanks!

    Can you clarify that the method you use to detect collectables and collisions is the best approach to use? I mean is this the most popular way most game developers use when using a tool such as Tiled?

    I ask this because I notice that the ninja does not always reach the wall set as a collision, probably because the wall has half background, half wall in the tile so it makes it appear that there is an invisible barrier between the ninja and the wall. I'm thinking different artwork would resolve this?

    Thanks again ;)
    elpuerco63
  • elpuerco63 Thanks for the kind words.

    Welcome to the world of programmer art :) This art comes from Tiled and was used to test functionality. So yeah, you can definitely get weird looking artifacts because the collision detection is on the entire tile.

    There are definitely other ways to do collision detection. I'm not sure which of them are the most popular.
    Charlie Fultoncharlie
  • Great tutorial!!!!
    but when I tried to run the project, I am getting following error:

    Cocos2d.h: No such file or directory.

    I tried to add my cocos2d files but no luck.
    How can I add cocos2d library to existing project?

    Thanks a lot!
    romox
  • romox wrote:Great tutorial!!!!
    but when I tried to run the project, I am getting following error:

    Cocos2d.h: No such file or directory.

    I tried to add my cocos2d files but no luck.
    How can I add cocos2d library to existing project?

    Thanks a lot!


    Hey, the sample project did not have cocos2d included. To add it to the sample project or any other existing project do these steps:


      download cocos2d 2.1 rc0
      in finder browse to the location of the download
      drag the file cocos2d-ios.xcodeproj into your Xcode project.
      then setup your build phases and build settings like it mentions in the tutorial



    Oh one more thing the sample project expects cocos2d (via relative path setting) in ~/Downloads/cocos2d-iphone-1.1-RC0 so if you download cocos2d to that direct everything should be good to go.

    I'll try to upload a new project today that already has cocos2d setup as an external reference in it.
    Charlie Fultoncharlie
  • Great tutorial(as always) but Im on a small detail..


    Here's the deal I just finished collision part and Im trying to run the game but it pauses...It compiles but then the simulator automatically pauses(yes, I checked if there was any breaks..) and it points out something like this:



    Code: Select all
    -(CGRect) rectForGID:(unsigned int)gid
    {
       CGRect rect;
       rect.size = tileSize_;

       gid &= kCCFlippedMask;
       gid = gid - firstGid_;

       int max_x = (imageSize_.width - margin_*2 + spacing_) / (tileSize_.width + spacing_);
       //   int max_y = (imageSize.height - margin*2 + spacing) / (tileSize.height + spacing);

       rect.origin.x = (gid % max_x) * (tileSize_.width + spacing_) + margin_;
       rect.origin.y = (gid / max_x) * (tileSize_.height + spacing_) + margin_;

       return rect;
    }


    On rect.origin.x there's a green arrow with the following info:

    Thread 1: EXC_ARITHMETIC(code=EXC_1386_DIV,subcode=0x0)

    Here's the debugger:

    cocos2d: **** WARNING **** CC_ENABLE_GL_STATE_CACHE is disabled. To improve performance, enable it by editing ccConfig.h

    Code: Select all
    2013-04-17 11:04:47.058 TileGame[12552:15203] cocos2d: cocos2d v2.0.0
    2013-04-17 11:04:47.058 TileGame[12552:15203] cocos2d: Using Director Type:CCDirectorDisplayLink
    2013-04-17 11:04:47.408 TileGame[12552:15203] cocos2d: animation started with frame interval: 60.00
    2013-04-17 11:04:47.410 TileGame[12552:15203] cocos2d: surface size: 480x320
    2013-04-17 11:04:47.613 TileGame[12552:15203] cocos2d: CCTexture2D: Using RGB565 texture since image has no alpha
    2013-04-17 11:04:49.480 TileGame[12552:15203] -[CCFileUtils fullPathFromRelativePath:resolutionType:] : cocos2d: Warning: File not found: TileGameResources/tmw_desert_spacing.png
    2013-04-17 11:04:49.482 TileGame[12552:15203] cocos2d: CCTexture2D. Can't create Texture. cgImage is nil
    2013-04-17 11:04:49.483 TileGame[12552:15203] cocos2d: Couldn't add image:TileGameResources/tmw_desert_spacing.png in CCTextureCache
    (lldb)


    From what I can tell it isn't finding the files but they're there....

    I tried redoing the map but still didn't work.

    Also, I posted something on PT.1 of this tutorial, I don't know if it has something to do with it.
    http://www.raywenderlich.com/forums/viewtopic.php?f=20&t=6506&p=39980#p39980
    GumbiRo
  • Hello everyone!

    So...I decided to do everything myself once again from scratch. I hadn't noticed but the map takes the information previously given to it for the tiles...(I had move the entire project to another folder before the error). It turns out the map wasn't finding the files so I had to redo the map again with the correct paths.

    And it worked like a charm!(hope this helps someone else in the future)

    Have a great day everyone!
    GumbiRo
  • Hey glad you figured it out. If you still have the old project around you can actually manually edit the tile map files, tmx file to point to the correct location using the correct path to the image file.
    Charlie Fultoncharlie
  • I have been going through these awesome tutorials for a while and trying to make the next BIG game...lol... I have started over I think a hundred times. So now I am working on the whole walking through walls part. I am using the accelerometer and here is my code from that:
    Code: Select all

    - (void)update:(ccTime)dt {
         [self updateOfficer];
       // [self setPlayerPosition:_officer.position];
        [self setViewPointCenter:self.officer.position];
    }

    -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { //new
        //constant of 6.0 is for tilt of device calibration
        officerSpeedY = 6.0 + acceleration.x*10;
        officerSpeedX = -acceleration.y*10;
    }

    -(void)updateOfficer { //new
        [self setPlayerPosition:_officer.position];
       
        float maxY = 1600 - _officer.contentSize.height/2;
        float minY = _officer.contentSize.height/2;
        float newY = _officer.position.y + officerSpeedY;
        newY = MIN(MAX(newY, minY), maxY);
       
        float maxX = 1600 - _officer.contentSize.width/2;
        float minX = _officer.contentSize.width/2;
        float newX = _officer.position.x + officerSpeedX;
        newX = MIN(MAX(newX, minX), maxX);
         
        _officer.position = ccp(newX, newY);
        CGPoint moveTo = _officer.position;
        [_officer moveToward:moveTo];
    }


    That works great... I think. So I think I am following the example pretty close and here is what I have.
    Code: Select all

    -(void)setPlayerPosition:(CGPoint)position {
        CGPoint tileCoord = [self tileCoordForPosition:position];
        int tileGid = [_meta tileGIDAt:tileCoord];
        if (tileGid) {
            NSDictionary *properties = [_tileMap propertiesForGID:tileGid];
            if (properties) {
                CCLOG(@"NSDictionary 'properties' contains:\n%@", properties);
                NSString *collision = properties[@"Collidable"];
                if (collision && [collision isEqualToString:@"True"]) {
                    NSLog(@"coll");
                    return;
                }
            }
        }
        _officer.position = position;
    }

    - (CGPoint)tileCoordForPosition:(CGPoint)position {
        int x = position.x / _tileMap.tileSize.width;
        int y = ((_tileMap.mapSize.height * _tileMap.tileSize.height) - position.y) / _tileMap.tileSize.height;
        return ccp(x, y);
    }

    So when my sprite hits the wall he just runs right through it. In the console it is outputting that the properties are there and it is true:
    Code: Select all

    2013-05-09 14:21:40.391 10-8[5447:907] NSDictionary 'properties' contains:
    {
        Collidable = True;
    }

    any clue why this is happening?
    mykdw
  • I found it bit of frustrating after building/running the game on my iOS simulator,
    the dixdoface ninja(no offence, that's what my ex-gf call this tiny ninja) appears kinda deviate from the place it supposed to display, like a little higher/ left.

    finally figured out it's something about ANCHOR, it's mentioned at the first cocos2d simple game example from great raywenderlich's tutorial.

    the _player.anchor was 0.5, 0.5 (by default)

    so i added below at the -(void)init, before the [self addChild:_player];

    Code: Select all


    _player.anchorPoint = ccp(0, 1);
    [self addChild:_player];



    now the little ninja is appeared at the correct place.
    thanks RAYWENDERLICH's great tutorial.
    quaker_z
  • Thank for your sharing. I have put it on the Github.(https://github.com/liuchang8877/FFTieldMapForNinja) This is the code and It can work well on the simulator ,but I find that it can not be well on the Iphone 4s .who can tell me why?
    liuchang8877
  • I 've been re-coding this tutorial in Cocos2d-x on Android, using Eclipse IDE on Ubuntu 12.04 LTS. It took me quite long because of some newer API, but finally I 've made it to the part of loading audio effects.

    But now I'm facing another strange thing. When using preloadEffect I get the following error at run time (happen when my ninja is about to pick up the cactus):

    Code: Select all
    OPENSSL ENGINE.CPP: file not found! Stop preload file: pickup.caf


    I 've rechecked the Resources folder, assets folder and the relative paths... Nothing 's seem wrong... Still the error 's raised >"<

    If any of you 've experienced this, plz let me know... Or it's just another tricky thing of Eclipse IDE >"<
    Obelisk
  • Maybe I figured it out. The sound must be in root of assets, not in, like, assets/TileGame/.../sound.caf.

    After that I've encountered another error about AudioFlinger, said "init failed", error code is -1.

    It 's seem the Android audio player (which is used by SimpleAudioEngine of cocos2dx - android) doesn't support .caf file by default.

    I 've get the job done with some alternative effects (.wav, .mp3) from other game resource pages.
    Obelisk
  • Anyone know if removeTileAt method has been depreciated? The coordinates seem fine, but the tile doesn't disappear. Anyone know of any other way of removing a tile?

    Thanks!!
    timinlondon

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!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: How to Make a Simple 2D Game with Metal.

Suggest a Tutorial - Past Results

Hang Out With Us!

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


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

... 53 total!

Update Team

  • Ray Fix

... 14 total!

Editorial Team

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!