Chapter    10

Working with Tilemaps

In the next two chapters, I introduce you to the world of tile-based games. Whether you’ve been playing games since the age of classic role-playing games like Ultima or you’ve just recently joined your Facebook friends in Farmville, I’m sure you’ve already played a game that uses the tilemap concept for displaying its graphics.

In tilemap games, the graphics consist of a small number of images, called tiles, which align with each other; placing them on a grid allows you to build rather convincing game worlds. The concept is very attractive because it conserves memory, compared to drawing the whole world as individual textures, while still allowing a lot of variety.

This chapter introduces general tilemap concepts by using the simplest tilemaps of all: orthogonal tilemaps. They’re most often built from square tiles, rarely from non-square rectangular tiles, and typically display the world in a top-down fashion. This chapter discusses the various display styles of tilemaps, and the next chapter focuses on isometric tilemaps, building on the tilemap programming basics that you’ll learn in this chapter.

What Is a Tilemap?

Tilemaps are 2D game worlds made of individual tiles. You can create large world maps with just a handful of images that all have the same dimensions. This means tilemaps are very efficient at conserving memory for large maps (game worlds). It’s no wonder that they first appeared in the early days of computer games. Many classic role-playing games used square tiles to create fantastic fantasy worlds. These games looked a bit like the tilemap in Figure 10-1, which is also a perfect example of an orthogonal tilemap. It looks like an aerial view, looking straight down. For this reason, orthogonal tilemaps usually look flat.

9781430244165_Fig10-01.jpg

Figure 10-1 .  An orthogonal tilemap in Tiled (Qt) Map Editor

Tiles in an orthogonal tilemap actually don’t need to be square; you can create orthogonal tilemaps from rectangular images as well. Those were most often used by Asian role-playing games, such as Dragon Quest. While still using an orthogonal perspective, it allowed the designers to create objects that are seemingly taller than wide. This enabled the designers to create the illusion of volumes, like houses, by painting them as several tiles and allowing game characters to be partially obstructed by tiles.

That method really came to shine with Ultima 6 and in particular Ultima 7. By skewing the perspective with which tiles were drawn, the effect came close to isometric tilemaps while still being orthogonal tilemaps. Using this method, the designers were able to create the illusion of depth, as you can see in Figure 10-2.

9781430244165_Fig10-02.jpg

Figure 10-2 .  An orthogonal tilemap with perspective tiles designed to be created in multiple layers to give the impression of depth. This tilemap style came to fame with Ultima 7

Isometric tilemaps take this one step further by not just drawing the tiles in a certain perspective but also by rotating them by 45 degrees. Isometric tilemaps are very effective in tricking our minds into believing that there really is a third dimension in this world, even though all the images are still essentially flat. Isometric tilemaps achieve this impression of depth by using tile images that are drawn as diamond shapes (rhombuses) and allowing tiles closer to the viewer to draw over the tiles further away from the viewer. See Figure 10-3 for an example of an isometric tilemap. I discuss isometric tilemaps in the next chapter in greater detail.

9781430244165_Fig10-03.jpg

Figure 10-3 .  An isometric tilemap in Tiled (Qt) Map Editor

The tilemaps in Figures 10-2 and 10-3 prove that tilemaps don’t need to be flat-looking. You can use the layering or stacking of tiles with some games that allow players to interact with the game world, as many of the Farmville fan videos show to great effect. Several Farmville users have used nothing but crop fields to build houses and even tall skyscrapers just by stacking tiles. They make use of the optical illusion that’s possible with isometric tilemaps.

You usually edit tilemaps with an editor, and the one directly supported by cocos2d is called Tiled (Qt) Map Editor. Tiled is free, is open source, and allows you to edit both orthogonal and isometric tilemaps with multiple layers. Tiled also enables you to add trigger areas (object layer), which you can use in a game to trigger certain actions when a character enters the area. They also serve as a way to add arbitrary positions to the map so that you can, for example, define spawn locations. By editing tile properties, you can determine what type of tile it is. You can also use this to block characters from entering certain tiles or, for example, to take damage when moving over a lava tile.

Note  Qt refers to Nokia’s Qt framework, on which Tiled is built. Because there’s also a defunct Java version of Tiled, it’s important to make this differentiation by writing Tiled (Qt). The Java version is no longer updated but contains a few extra features that may be worth checking out. In this and the following chapter, I use and discuss Tiled (Qt).

Preparing Images with TexturePacker

Before you start working with the Tiled (Qt) Map Editor, you need to prepare the tilemap graphics. The set of tile images for a tilemap is commonly called a tileset. Technically it’s just a texture atlas containing tile images, but Tiled (Qt) can only load tilesets in PNG (recommended) or JPG format.

In the Tilemap01 project for this chapter, you’ll find a number of square tile images in the Assets/tiles folder. Add all these tile images to TexturePacker and then uncheck Allow Rotation, set Algorithm to Basic, and set Sort by to Name. These settings ensure that the tiles are ordered and aligned correctly for Tiled. The resulting tileset in TexturePacker should look similar to Figure 10-4.

9781430244165_Fig10-04.jpg

Figure 10-4 .  Creating a texture atlas of a few square tiles using TexturePacker

For Tiled it’s crucial that the tiles stay at the same position—this is why sorting by name and disallowing rotation is so important—because Tiled refers to individual tiles in the tileset by position (tile index) and offset only. That means that if tiles in the texture change places, the tilemap in Tiled will look completely different. The tilemap still refers to the same tile positions in the tileset, but instead of a grass tile there might now be a water tile at that position, for example.

Tiled (Qt) Map Editor

The most popular tool to create tilemaps for use with cocos2d is the Tiled (Qt) Map Editor that I’ve already mentioned in the preceding sections. The TMX files it creates are natively supported by the cocos2d game engine. TMX files are simply XML files that, if need be, you could edit with a text editor.

Tiled is available as a free download, and at the time of this writing the latest version is 0.8.1. You can download Tiled from its home page, at www.mapeditor.org.

If you’d like to support the development of Tiled, consider making a donation to the project’s developer, Thorbjørn Lindeijer. You can find the donate button on the web site.

Creating a New Tilemap

The first thing after you’ve downloaded and started Tiled is to go to the View menu and check both the Tilesets and Layers items. This will show the list of layers and the current tileset on the right-hand side of the Tiled window which you’ll need to refer to frequently. Then choose File image New to create a new tilemap, bringing up the New Map dialog pictured in Figure 10-5.

9781430244165_Fig10-05.jpg

Figure 10-5 .  Creating a new tilemap in Tiled

Currently, Tiled supports orthogonal (rectangular) tilemaps as well as isometric tilemaps. Hexagonal maps are not supported. The map size is given in tiles, but Tiled will show you the size of the map in pixels as well. In this case, the new map will be 30 × 20 tiles, with the tile images having 32 × 32-pixel dimensions. It’s crucial that the tile size matches the size of your tile images, or they will not align.

The new map will be completely empty, and there’s no tileset loaded that you can draw from. You can add a tileset by selecting Map image New Tileset from Tiled’s menu. This opens the New Tileset dialog (shown in Figure 10-6), where you can browse for the proper tileset image. A tileset is just a name for an image containing multiple tiles with equal spacing, so you could also call it a texture atlas containing only images of the same size.

9781430244165_Fig10-06.jpg

Figure 10-6 .  Creating a new tileset from an image file

Note  The function Map image Add External Tileset is used only to import a previously exported tileset to share the same tileset with multiple TMX maps. You can export a tileset by right-clicking the tileset view in the lower right-hand corner of the Tiled window and then choosing Export Tileset As.

You’ll use the dg_grounds32.png tileset. These tiles were drawn by David E. Gervais and published under the Creative Commons License, meaning you are free to share and remix his work as long as you give credit to him. You can download more of his tilesets from this web site: http://pousse.rapiere.free.fr/tome/index.htm.

In Figure 10-6, I have already added the dg_grounds32.png tileset image by locating it through the Browse button in the Resources folder of the Tilemap01 project. If you check the “Use transparent color” check box, transparent areas will simply be drawn in pink (the default color). You can leave this box unchecked for now because the tiles in use have no transparent areas.

The tile width and height are the dimensions of individual tiles in the tileset. They should match the tile size of 32 × 32 pixels, which you set when creating the new map. The Margin and Spacing settings determine how many pixels away from the image border the tiles are and how many pixels of space are between them. In the case of the dg_grounds32.png, there’s no spacing at all, so I set both values to 0.

If you had your tiles aligned by TexturePacker to create a tileset texture, you must enter the pixel-padding value used by TexturePacker in the Margin and Spacing fields. By default, TexturePacker uses a padding of 0 pixels.

When loading a new tileset image, make sure the tileset image is already located in your project’s Resource folder. You should then also make sure to save the tilemap TMX file to the same folder where the tileset image used by the tilemap is located. Otherwise, cocos2d might fail to load the tileset image; trying to load the TMX file will then cause a runtime exception. The culprit is that TMX files reference the tileset image relative to the location the TMX file is saved to. If they’re not both in the same folder, cocos2d may be unable to find the image because the folder structure isn’t preserved when the app is installed in the simulator or on the device.

TIP  TMX files are plain XML files, so you might want to peek inside if you’re curious. If you see the image file referenced with parts of a path, then cocos2d is unlikely to load the referenced image file. The image reference should list just the image filename without any path components, like so: <image source = "tiles.png"/>.

How to create Tilemaps for Retina and iPad Resolutions

Tiled has no concept of varying resolutions for tilemaps. But cocos2d supports all the usual extensions like -hd, -ipad and -ipadhd for tilemap files. Problem is, how to convert an existing tilemap to an HD tilemap or vice versa?

The answer lies in manually editing the TMX files created by Tiled in Xcode or with a text editor. Following is the entire contents of the tilemap.tmx file used in this chapter:

<?xml version = "1.0" encoding = "UTF-8"?>
<map version = "1.0" orientation = "orthogonal" width = "24" height = "16" tilewidth = "40" tileheight = "40">
 <tileset firstgid = "1" name = "tiles" tilewidth = "40" tileheight = "40" spacing = "0" margin = "0">
  <image source = "tiles.png" width = "128" height = "128"/>
 </tileset>
 <layer name = "Tile Layer 1" width = "24" height = "16">
  <data encoding = "base64" compression = "zlib">
eJztlN0KgCAMRr9Km+//xBEssLU/Qe8SDuqEM2VMAGgLAc80mRH/Nojl3ycR + Q9G7mU8k0P6H0dhpDubR/OTcHt4OUnxazXI5Cn4vsXz31TmZPq1RcW7rpq/P4t8GqS45P2tPoz6Kev3erEZscz/sPL/+Yc9LqVyBq0=
  </data>
 </layer>
</map>

I’ve highlighted in bold the entries you need to modify. Because this is a standard-resolution tilemap, you need to multiply all the numeric values by two to convert the tilemap to a Retina resolution tilemap. Be careful not to change the map’s parameters by accident, especially because both map and tileset have tilewidth and tileheight parameters. And if you convert a Retina tilemap to standard resolution, you’d divide the numeric values by two. In that case all the values should be even numbers so they’re divisible by two with no remainder.

You also need to change the source image name so that it uses the tiles-hd.png image that you can create with TexturePacker as described in Chapter 6. Then save the modified tilemap as tilemap-hd.tmx.

Unfortunately this process is tedious and error-prone because you have to do this after every modification to the tilemap, or you can choose to edit both standard and Retina resolution tilemaps at the same time—but that’s still twice the work, or four times the work if you want to support standard, Retina, iPad, and Retina iPad resolutions. In that case, you’d have to keep these files up to date:

  • Standard resolution: tilemap.tmx and tilemap.png
  • Retina resolution: tilemap-hd.tmx and tilemap-hd.png
  • iPad resolution: tilemap-ipad.tmx and tilemap-ipad.png
  • iPad Retina resolution: tilemap-ipadhd.tmx and tilemap-ipadhd.png

You can always design and test your tilemap in one resolution and only convert it on occasion to other resolutions. You might also consider writing a script or a small app that automates that process for you. A good starting point is the Tilemap HD/SD conversion tool available here: http://wasabibit.com/WasabiBit/Dev_Notes.html.

The alternative is to create your tilemaps with iTileMaps on an iOS device. It lets you automatically create SD/HD versions of the tilemaps you’re creating. You can try out the free version to see if iTileMaps is an option for you. You can find more information on the iTileMaps homepage: www.klemix.com/page/iTileMaps.aspx.

Designing a Tilemap

With the tileset loaded, you’re faced with a blank map, an invitation for your creativity to come up with great ideas for a tilemap. What’s even better is to get rid of the blank tilemap as the very first step. It’s very helpful to start the tilemap with a default floor tile. I selected the Bucket Fill tool and picked a bright grass tile so that my tilemap is now a lush meadow—sort of. You can see it in Figure 10-7.

9781430244165_Fig10-07.jpg

Figure 10-7 .  An empty map with the dg_grounds32 tileset loaded, waiting for your inspiration

Tip  If you ever notice something missing from Tiled’s window, check the View menu. You can hide and unhide the Tilesets, Layers, and History views. This is important because it’s the only way to bring back these views in case you clicked the X button on one of these views.

Tiled uses four modes for editing the tilemap, indicated by the four rightmost icons on the toolbar. They are Stamp Brush (hotkey B), which allows you to draw the current selection in the tileset; Bucket Fill (hotkey F), which fills areas of connected, identical tiles; Eraser (hotkey E), which erases tiles; and Rectangular Select (hotkey R), with which you can select a range of tiles and then copy and paste the selection.

You can also zoom the tilemap. If you have a mouse with a scroll wheel, hold down the Command key and roll the scroll wheel to zoom in and out. Alternatively, you can also zoom in and out by pressing the Command key with either the plus and minus sign, respectively.

You’ll spend most of your time picking a tile from the tileset and drawing it onto the tilemap with the Stamp Brush selected. Placing tile by tile, you’ll create your tile-based game world.

You can also edit tiles in multiple layers by adding more layers in the Layers view. From the menu, choose Layer image Add Tile Layer to create a new layer for tiles. Using multiple tile layers allows you to switch out areas of the tilemap in cocos2d. In the TileMap01 example project, I use it to switch parts of the map between winter and summer.

You can also choose Layer image Add Object Layer to add a layer for adding objects. There are two types of objects: the regular objects are simply rectangles that you can place, and the other type of objects are tile objects that let you freely place a tile anywhere on the map, without snapping to the tile grid. You can use the rectangle objects to annotate the map with custom information, such as spawn points, teleport locations, or trigger areas. The tile objects are most commonly used to place smaller things like swords, flowers, candles, and other items directly onto the tile world.

To work with objects, additional buttons are available in the Tiled toolbar: Select Objects (hotkey S), Insert Objects (hotkey O), and Insert Tile Objects (hotkey T). To insert a rectangle object, click the Insert Objects icon and click in the tilemap world to create point objects (rectangles with zero width and height), or click and drag down and to the right to create a rectangle. To insert a tile object, click the Insert Tile Objects icon, select a tile from the tileset, and then click the tilemap world to add a new tile object.

Some functionality in Tiled is hidden in context menus. For example, you can delete the rectangle objects I just mentioned by right-clicking them in the Tilemap view and selecting Remove Object. Note that you also need to have the object layer highlighted in the Layers list view for the context menu to appear.

You can also edit properties of objects, layers, and tiles by right-clicking them and clicking the corresponding Properties menu item. One use for that is to create an additional tile layer by using Layer image Add Tile Layer. This will be a layer used to tell the game about certain properties of tiles. I called it GameEventLayer because it will be used to define trigger areas for certain game events.

With GameEventLayer selected, choose Map image New Tileset and load game-events.png from the same folder as dg_grounds32.png. It contains only three tiles. Right-click one, select Tile Properties, and add the isWater property, as shown in Figure 10-8.

9781430244165_Fig10-08.jpg

Figure 10-8 .  Adding a tile property

Caution  Keep in mind that each tile layer creates some overhead, especially if you set tiles in multiple layers at the same location. This will cause both layers to be drawn and can adversely affect the game’s performance. I recommend keeping the number of tile layers to a minimum. Two to four tile layers should be sufficient for most games. Be sure to take a look at your game’s framerate on the device after you’ve added a new tile layer and drawn on it.

Also be aware that Tiled allows you to add more than one tileset for a layer. However, cocos2d supports only one tileset per layer.

You can then draw over the tilemap using the tile whose properties you just added the isWater property to. Ideally, draw it over the river. To see the tiles underneath what you’re drawing, you can use the Opacity slider for the GameEventLayer in the Layers view. Or click the layer’s check box to hide or unhide everything drawn on this particular layer.

Just be sure to enable all layer check boxes before saving the TMX tilemap. Cocos2d doesn't load layers that are unchecked in Tiled.

When you’re done with this, you should have a tilemap similar to the one in Figure 10-9. Save it in the same TileMap01 Resources folder where the tileset images are.

9781430244165_Fig10-09.jpg

Figure 10-9 .  A completed tilemap with three tile layers and an object layer

Using Orthogonal Tilemaps with Cocos2d

To use TMX tilemaps with cocos2d, you first have to add the TMX file and the accompanying tileset image files as resources to your Xcode project. In the TileMap01 project, I added orthogonal.tmx along with the tilesets dg_grounds32.png and game-events.png. Loading and displaying the tilemap is very straightforward; the following code is the interface of the TileMapLayer class:

#import "cocos2d.h"
 
enum
{
  TileMapNode = 0,
};
 
@interface TileMapLayer : CCLayer
{
}
 
+(id) scene;
@end

And this is the TileMapLayer implementation which loads a tilemap and hides the GameEventLayer:

#import "TileMapLayer.h"
#import "SimpleAudioEngine.h"
 
@implementation TileMapLayer
+(id) scene
{
  CCScene *scene = [CCScene node];
  TileMapLayer *layer = [TileMapLayer node];
  [scene addChild: layer];
  return scene;
}
 
-(id) init
{
  if ((self = [super init]))
  {
  CCTMXTiledMap* tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"orthogonal.tmx"];
  [self addChild:tileMap z:-1 tag:TileMapNode];
 
  CCTMXLayer* eventLayer = [tileMap layerNamed:@"GameEventLayer"];
  eventLayer.visible = NO;
 
  self.isTouchEnabled = YES;
  }
  return self;
}
@end

The CCTMXTiledMap class is initialized with the name of the TMX file and then added as a child with a tag so that it can be retrieved later. An instance variable would of course work just as well. The next step is to retrieve the CCTMXTiledMap used for game events by using the layerNamed method and providing the name of the layer as it was named in Tiled. Because the game events layer will be used only as hints for code to determine properties of certain tiles, this layer shouldn’t be rendered at all. Note that if you uncheck the layer in Tiled, it won’t be displayed, but you won’t have access to its tiles and tile properties either.

If you run the project now, you’ll see a tilemap just like in Figure 10-10.

9781430244165_Fig10-10.jpg

Figure 10-10 .  The orthogonal tilemap in the iPhone Simulator

Right now you can’t do anything with the tilemap, but I’d like to change that. For example, I’d like to be able to find the isWater tiles. I’ve added the ccTouchesBegan method, as shown in Listing 10-1, to determine the tile that the player is touching.

Listing 10-1.  Determining a Tile’s Properties

-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  CCNode* node = [self getChildByTag:TileMapNode];
  NSAssert([node isKindOfClass:[CCTMXTiledMap class]], @"not a CCTMXTiledMap");
  CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
 
  // Get the position in tile coordinates from the touch location
  CGPoint touchLocation = [self locationFromTouch:touches.anyObject];
  CGPoint tilePos = [self tilePosFromLocation:touchLocation tileMap:tileMap];
 
  // Check if the touch was on water (e.g., tiles with isWater property)
  BOOL isTouchOnWater = NO;
  CCTMXLayer* eventLayer = [tileMap layerNamed:@"GameEventLayer"];
  int tileGID = [eventLayer tileGIDAt:tilePos];
 
  if (tileGID ! = 0)
  {
  NSDictionary* properties = [tileMap propertiesForGID:tileGID];
  if (properties)
  {
  NSString* isWaterProperty = [properties valueForKey:@"isWater"];
  isTouchOnWater = isWaterProperty.boolValue;
   }
  }
 
  // Decide what to do depending on where the touch was
  if (isTouchOnWater)
  {
  [[SimpleAudioEngine sharedEngine] playEffect:@"alien-sfx.caf"];
  }
  else
  {
  // Get the winter layer and toggle its visibility
  CCTMXLayer* winterLayer = [tileMap layerNamed:@"WinterLayer"];
  winterLayer.visible = !winterLayer.visible;
  }
}

The CCTMXTiledMap is retrieved as usual. The location of the touch is first converted into screen coordinates and then used to retrieve the tilePos containing the indices into the tilemap at that specific screen location. I’ll get to the tilePosFromLocation method in a minute. For now, just know that it returns the index of the touched tile.

At this point, I have to introduce the concept of global idenjpgiers (GIDs) for tiles, which are unique integer numbers assigned to each tile used in a tilemap. The tiles in a map are consecutively numbered, starting with 1. A GID of 0 represents an empty tile. With the tileGIDAt method of the CCTMXLayer, you can determine the GID number of the tile at the given tile coordinates.

Next, the CCTMXLayer named GameEventLayer is obtained from the tilemap. This is the layer where I defined the isWater tile and drew it over the river tiles. The tileGIDAt method returns the unique idenjpgier for this tile. If the idenjpgier happens to be 0, it means there is no tile at this position on this layer—in that case, it’s already clear that the touched tile can’t be an isWater tile.

The CCTMXTiledMap has a propertiesForGID method, which returns an NSDictionary if there are properties available for the tile with the given idenjpgier (GID). This NSDictionary contains the properties edited in Tiled (see Figure 10-8). The dictionary stores any key/value pairs as NSString objects. To see what’s in a particular NSDictionary for debugging purposes, you can use a CCLOG statement like this:

CCLOG(@"NSDictionary 'properties' contains:
%@", properties);

This will print out a line similar to the following in the Debugger Console window:

2010–08-30 19:50:52.344 Tilemap[978:207] NSDictionary 'properties' contains:
{
  isWater = 1;
}

You’ll be dealing with a variety of NSDictionary objects while working with tilemaps. Logging its contents allows you to peek inside any NSDictionary, or any iPhone SDK collection class, for that matter. This will frequently come in handy.

You can retrieve each property in an NSDictionary by its name through the NSDictionary method valueForKey, which returns an NSString. To get a Bool value from the NSString, you can simply use the NSString’s boolValue property. In much the same way, you can retrieve integer and floating-point values using NSString’s intValue and floatValue properties, respectively.

At the end of ccTouchesBegan, I check whether the touch was on water, and if so, a sound is played. Otherwise, I retrieve the WinterLayer and toggle its visible property by negating it. Changing seasons has never been this simple! The effect should illustrate how you can use multiple layers in Tiled to achieve changes on a global scale without having to load a completely separate tilemap.

For more local changes to individual tiles, you can use the removeTileAt and setTileGID methods to remove or replace tiles of a specific layer during game play:

[winterLayer removeTileAt:tilePos];
[winterLayer setTileGID:tileGID at:tilePos];

Locating Touched Tiles

I mentioned the tilePosFromLocation method earlier, and I’ll repeat the two relevant lines from the ccTouchesBegan code here before getting to the implementation of locationFromTouch and tilePosFromLocation:

// Get the position in tile coordinates from the touch location
CGPoint touchLocation = [self locationFromTouch:touches.anyObject];
CGPoint tilePos = [self tilePosFromLocation:touchLocation tileMap:tileMap];

First, the position of the touch is mapped to screen coordinates. I’ve done this before, but because you’ll be needing this code a lot, I’ve provided it in Listing 10-2 for your reference.

Listing 10-2.  Determining the Position of a Touch

-(CGPoint) locationFromTouch:(UITouch*)touch
{
  CGPoint touchLocation = [touch locationInView:touch.view];
  return [[CCDirector sharedDirector] convertToGL:touchLocation];
}

With the touch location converted to screen coordinates, the tilePosFromLocation method is called. It gets both the touch location and a pointer to the tileMap as parameters. The method in Listing 10-3 contains a bit of math, which I’ll explain in a second—hold your breath:

Listing 10-3.  Converting Location to Tile Coordinates

-(CGPoint) tilePosFromLocation:(CGPoint)location tileMap:(CCTMXTiledMap*)tileMap
{
  // Tilemap position must be offset, in case the tilemap is scrolling.
  CGPoint pos = ccpSub(location, tileMap.position);
 
  // scaling tileSize to Retina display size
  float pointWidth = tileMap.tileSize.width / CC_CONTENT_SCALE_FACTOR();
  float pointHeight = tileMap.tileSize.height / CC_CONTENT_SCALE_FACTOR();
 
  // Cast to int makes sure that result is in whole numbers
  pos.x = (int)(pos.x / pointWidth);
  pos.y = (int)((tileMap.mapSize.height * pointHeight - pos.y) / pointHeight);
 
  // Ensure coordinates are always within tilemap bounds.
  pos.x = fmaxf(0, fminf(tileMap.mapSize.width - 1, pos.x));
  pos.y = fmaxf(0, fminf(tileMap.mapSize.height - 1, pos.y));
 
  return pos;
}

Still with me? If you’ve worked with tilemaps before, this bit of code should be familiar, but if not, you may be at a loss. I’ll explain. The first thing this method does is subtract the current tileMap.position from the touch location. The upcoming changes to the Tilemap project will add tilemap scrolling, so the tilemap’s position will most likely not be at 0,0, and that needs to be factored in by subtracting the tilemap’s position from the location.

To make the viewpoint scroll farther up (north) and to the right (east), you actually have to change its position by negative amounts. That’s because the tilemap starts at position 0,0, which positions the map’s bottom left-hand corner at the very bottom left of the screen. The tilemap’s 0,0 point coincides with the screen’s 0,0 point initially. If you were to move the tilemap to position 100,100, it would seem as if the viewpoint were moving toward the left and down. The common mistake is to assume that you’re moving the viewpoint, which you are not. The tilemap layer is what’s moving, and to scroll farther toward the center of the tilemap, you have to offset the tilemap by negative values.

The rest is simple math: to get the proper offset from the tilemap (whose position you know is always negative), you have to subtract the touch location and tileMap.position. The concrete numbers reveal that subtracting a negative number is actually an addition:

location(240, 160) – tileMap.position(−100, -100) = pos(340, 260)

With the tilemap layer moved −100,–100 pixels away from the screen’s 0,0 point, and the touch being at 240,160 pixels on the screen, the total offset of the touch location from the tilemap’s position is 340,260 pixels away from the current tileMap.position.

With the scrolling offset taken into account, you can get the tile coordinates for the tile at this location into the tilemap. At this point, you have to consider that the tile coordinates’ 0,0 tile is at the top left-hand corner of the tilemap. Contrary to screen coordinates, where the 0,0 point (point of origin) is at the lower left-hand corner, the tilemap coordinates start at the upper left-hand corner. Figure 10-11 shows the x,y coordinates of a series of tiles. The screenshot was made with the Tiled Java version by enabling View image Show Coordinates, which is a feature that isn’t available yet in the Tiled Qt version.

9781430244165_Fig10-11.jpg

Figure 10-11 .  The coordinate system of an orthogonal tilemap

So you're not confused, here’s the code that calculates the tile coordinate’s x position:

float pointWidth = tileMap.tileSize.width / CC_CONTENT_SCALE_FACTOR();
pos.x = (int)(pos.x / pointWidth);

The tileMap.tileSize property is the size of the tiles in the tileset, which in this case is 32×32 (see also Figure 10-6). By dividing the tilemap’s tilesize with the CC_CONTENT_SCALE_FACTOR(), the width (and height) are converted to point coordinates. Internally, cocos2d handles tilemap sizes as pixel coordinates—this is one of the few exceptions where you’ll have to deal with conversion between pixel and point coordinates to ensure correct behavior on both standard- and Retina-resolution devices.

If the touch were at the 340 x position, the calculation would reveal the following:

340 / 32 = 10.625

That can’t be right, though. You’re looking for a tile’s x coordinate, which is never a fractional number! The reason is, of course, that the touch was somewhere inside the tile we’re looking for (that is, inside a 32×32 square area). The simple trick of casting the result to an int value will get rid of the fractional part and assign this to pos.x:

pos.x = (int)10.625 // pos.x == 10

Casting to an int will remove the fractional part. You can safely get rid of the fractional part because it’s simply not relevant—actually it’s harmful. If you didn’t cast away the fractional part but used the noninteger coordinate—in this example, 10.625—to try to retrieve the tile at a tile coordinate 10.625, you’d receive a runtime error because there is only a tile at x coordinates 10 and 11, not at 10.625.

You use a slightly more complicated calculation to get the tile’s y coordinate:

float pointHeight = tileMap.tileSize.height / CC_CONTENT_SCALE_FACTOR();
pos.y = (int)((tileMap.mapSize.height * pointHeight - pos.y) / pointHeight);

Note that the parentheses are important to make sure the division is done last. In actual numbers, this calculation may be easier to understand. As shown in Figure 10-5, the tileMap.mapSize is 30 × 20 tiles, and as I mentioned earlier, tileMap.tileSize is 32 × 32 pixels. The calculation then looks like this:

pos.y = (int)((20 * 32 – 260) / 32)

Multiplying tileMap.mapSize.height with pointHeight returns the full height of the tilemap in points. This is necessary because the tilemap starts counting y coordinates from top to bottom, whereas screen y coordinates count from bottom to top. By calculating the bottommost y coordinate of the tilemap and subtracting the current y position 260 from that, you get the correct y position of the touch into the tilemap, in points. And because it’s a point coordinate, which can have a fractional part, you need to divide by the pointHeight and then cast down to an int value to get the tile’s y coordinate.

Before the coordinates are returned, they’re clipped to valid coordinates within the tilemap width and size. The fminf and fmaxf functions return whichever of the two values is smaller or larger respectively; used in combination they clip the coordinates to within 0 and tilemap size or width minus 1. This is far shorter than using a series of if statements.

Working with the Object Layer

The orthogonal.tmx tilemap I’ve created as an example tilemap for this chapter also contains an object layer, fittingly named ObjectLayer. You can create object layers in Tiled by choosing Layer image Add Object Layer. Then you can click inside the tilemap and draw rectangles. I think the name object layer is a bit unfortunate and misleading, because most games will use these rectangles as points of interest and trigger areas, and not as actual objects.

In the Tilemap01 project, I’ve added a bit more code to the ccTouchesBegan method to interact with the object layer. Listing 10-4 shows the relevant part of the code, which follows directly after the isWater check:

Listing 10-4.  Detecting If a Touch Was Inside an ObjectLayer Rectangle

// Check if the touch was within one of the rectangle objects
CCTMXObjectGroup* objectLayer = [tileMap objectGroupNamed:@"ObjectLayer"];
 
BOOL isTouchInRectangle = NO;
int numObjects = objectLayer.objects.count;
for (int i = 0; i < numObjects; i++)
{
  NSDictionary* properties = [objectLayer.objects objectAtIndex:i];
  CGRect rect = [self getRectFromObjectProperties:properties tileMap:tileMap];
 
  if (CGRectContainsPoint(rect, touchLocation))
  {
  isTouchInRectangle = YES;
  break;
  }
}

Because object layers are a different kind of layer, you can’t get them via the layerNamed method of the tilemap. The object layer in cocos2d is the class CCTMXObjectGroup— another unfortunate naming mishap, because Tiled calls it an object layer, not an object group. In any case, you can get the CCTMXObjectGroup for the object layer named simply ObjectLayer by using the tilemap’s objectGroupNamed method and specifying the object layer’s name as defined in Tiled.

Next, I iterate over the objectLayer.objects NSMutableArray, which contains a list of NSDictionary items. Sound familiar? Yes, these are the same NSDictionary properties returned by the tilemap’s propertiesForGID method, as shown earlier—except that the contents of these NSDictionary items are given by Tiled and aren’t user-editable. They simply contain the coordinates for each rectangle. The method getRectFromObjectProperties returns the rectangle:

-(CGRect) getRectFromObjectProperties:(NSDictionary*)dict tileMap:(CCTMXTiledMap*)tileMap
{
  float x, y, width, height;
 
  x = [[dict valueForKey:@"x"] floatValue] + tileMap.position.x;
  y = [[dict valueForKey:@"y"] floatValue] + tileMap.position.y;
  width = [[dict valueForKey:@"width"] floatValue];
  height = [[dict valueForKey:@"height"] floatValue];
 
  return CGRectMake(x, y, width, height);
}

The keys x, y, width, and height are set by Tiled. I simply retrieve them from the NSDictionary via valueForKey and use the floatValue method to convert the values from NSString to actual floating-point numbers. The x and y values need to be offset with the tileMap’s position, because the rectangles need to be moving along with the tilemap. At the end, a CGRect is returned by calling the CGRectMake convenience method.

The code in ccTouchesBegan then simply checks whether the touch location is contained in the rect via CGRectContainsPoint. If it is, the isTouchInRectangle flag is set to true, and the for loop is aborted by using the break statement. There’s no need to check another rectangle for containing the touch location. At the end of ccTouchesBegan, the isTouchInRectangle flag is then used to decide whether to play a particle effect at the touch location. So, this code is creating an explosion particle effect whenever you touch inside a rectangle:

if (isTouchOnWater)
{
  [[SimpleAudioEngine sharedEngine] playEffect:@"alien-sfx.caf"];
}
else if (isTouchInRectangle)
{
  CCParticleSystem* system = [CCParticleSystemQuad particleWithFile:←
  @"fx-explosion.plist"];
  system.autoRemoveOnFinish = YES;
  system.position = touchLocation;
  [self addChild:system z:1];
}

Drawing the Object Layer Rectangles

When you run the book’s Tilemap01 project, you’ll notice that the object layer rectangles are drawn over the tilemap, as shown in Figure 10-12. This isn’t a standard feature of tilemaps or object layers. Instead, the rectangles are drawn using OpenGL ES code. Every CCNode has a –(void) draw method that you can override to add custom OpenGL ES code. I tend to use this a lot to debug my code visually by drawing lines, circles, and rectangles that may be used for collision and distance tests, among other things. In this case, actually seeing where the object layer areas are is very useful. Visualizing such information beats looking up and comparing coordinates in the debugger. Our minds are much better at assessing visual information than they are at comparing and calculating numbers. Use this fact to your advantage!

9781430244165_Fig10-12.jpg

Figure 10-12 .  The tilemap with object layer rectangles displayed using OpenGL ES code

The –(void) draw method just needs to be in the class, and it will be called automatically every frame. However, you should refrain from using the draw method to modify properties of nodes because that can interfere with drawing the nodes. Listing 10-5 shows the draw method of the TileMapLayer class.

Listing 10-5.  Drawing ObjectLayer Rectangles

#ifdef DEBUG
-(void) draw
{
  [super draw];
 
  CCNode* node = [self getChildByTag:TileMapNode];
  NSAssert([node isKindOfClass:[CCTMXTiledMap class]], @"not a CCTMXTiledMap");
  CCTMXTiledMap* tileMap = (CCTMXTiledMap*)node;
 
  // Get the object layer
  CCTMXObjectGroup* objectLayer = [tileMap objectGroupNamed:@"ObjectLayer"];
 
  // make the lines thicker
  glLineWidth(2.0f * CC_CONTENT_SCALE_FACTOR());
  ccDrawColor4F(1, 0, 1, 1);
 
  int numObjects = objectLayer.objects.count;
  for (int i = 0; i < numObjects; i++)
  {
  NSDictionary* properties = [objectLayer.objects objectAtIndex:i];
  CGRect rect = [self getRectFromObjectProperties:properties tileMap:tileMap];
 
  CGPoint dest = CGPointMake(rect.origin.x + rect.size.width, ←
  rect.origin.y + rect.size.height);
  ccDrawRect(rect.origin, dest);
  ccDrawSolidRect(rect.origin, dest, ccc4f(1, 0, 1, 0.3f));
  }
 
  // reset line width
  glLineWidth(1.0f);
}
#endif

First, I get the tilemap by its tag and then the CCTMXObjectGroup by using the objectGroupNamed method. I then set the line width to 2 pixels with the OpenGL ES method glLineWidth. Multiplying with CC_CONTENT_SCALE_FACTOR() ensures that the line width scales to 4 pixels on Retina devices, because CC_CONTENT_SCALE_FACTOR() returns 2.0f on Retina devices, otherwise 1.0 f. This affects line thickness and color of all subsequent lines drawn with OpenGL ES—not just in the current method but possibly other nodes that use OpenGL ES code for drawing (for example, any of the convenience methods for drawing lines, circles, and polygons defined in cocos2d’s CCDrawingPrimitives.h header file). That’s why I reset glLineWidth after I’m done drawing. It’s good style in OpenGL code to leave its state like you found it; otherwise, it might alter the way other draw code produces its output. OpenGL is a state machine, so every setting you change is remembered and may affect subsequent drawing methods. To avoid this, any OpenGL settings you change should be set back to a safe default after you’re done drawing.

Note  Code inside the -(void) draw method is always drawn at a z-order of 0. It’s also drawn before all other nodes at z-order 0, which means that any OpenGL ES code will be overdrawn by other nodes if they’re also at z-order 0. In the case of the object layer draw code, I had to add the tileMap at a z-order of −1 for the rectangles to be drawn over the tilemap.

Just as before, I iterate over all object layer objects and get their properties from NSDictionary to get the CGRect of that object, which is then used in the ccDrawRect and ccDrawSolidRect methods. Unfortunately, these methods don’t take a CGRect as input but an origin and destination CGPoint. Therefore, you have to create the destination point manually by adding the corresponding sizes to the origin coordinates.

Note that the draw method is enclosed in #ifdef DEBUG and #endif statements. This means that the object layer rectangles won’t be drawn in release builds, because they’re only needed for debugging and illustration purposes—the end user should never see them.

Scrolling the Tilemap

The best part comes last: scrolling. It’s actually straightforward because you only need to move the CCTMXTiledMap. In the Tilemap01 project, I’ve added the call to the centerTileMapOnTileCoord method in ccTouchesBegan right after obtaining the tile coordinates of the touch:

-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  ...
  // Get the position in tile coordinates from the touch location
  CGPoint touchLocation = [self locationFromTouches:touches];
  CGPoint tilePos = [self tilePosFromLocation:touchLocation tileMap:tileMap];
 
  // Move tilemap so that the touched tile is at the center of the screen
  [self centerTileMapOnTileCoord:tilePos tileMap:tileMap];

  ...
}

Listing 10-6 shows the centerTileMapOnTileCoord method, which moves the tilemap so that the touched tile is at the center of the screen. It also stops the tilemap from scrolling farther if any tilemap border already aligns with the screen edge.

Listing 10-6.  Centering the Tilemap on a Tile Coordinate

-(void) centerTileMapOnTileCoord:(CGPoint)tilePos tileMap:(CCTMXTiledMap*)tileMap
{
  // center tilemap on the given tile pos
  CGSize screenSize = [CCDirector sharedDirector].winSize;
  CGPoint screenCenter = CGPointMake(screenSize.width * 0.5f, ←
  screenSize.height * 0.5f);
 
  // tile coordinates are counted from upper left corner, this maps coordinates to lower left corner
  tilePos.y = (tileMap.mapSize.height - 1) - tilePos.y;
 
  // scaling tileSize to Retina display size
  float pointWidth = tileMap.tileSize.width / CC_CONTENT_SCALE_FACTOR();
  float pointHeight = tileMap.tileSize.height / CC_CONTENT_SCALE_FACTOR();
 
  // point is now at lower left corner of the screen
  CGPoint scrollPosition = CGPointMake(−(tilePos.x * pointWidth), ←
  -(tilePos.y * pointHeight));
 
  // offset point to center of screen and center of tile
  scrollPosition.x + = screenCenter.x - pointWidth * 0.5f;
  scrollPosition.y += screenCenter.y - pointHeight * 0.5f;
 
  // make sure tilemap scrolling stops at the tilemap borders
  scrollPosition.x = MIN(scrollPosition.x, 0);
  scrollPosition.x = MAX(scrollPosition.x, -screenSize.width);
  scrollPosition.y = MIN(scrollPosition.y, 0);
  scrollPosition.y = MAX(scrollPosition.y, -screenSize.height);
 
  CCLOG(@"tilePos: (%i, %i) moveTo: (%.0f, %.0f)",
  (int)tilePos.x, (int)tilePos.y, scrollPosition.x, scrollPosition.y);
 
  CCAction* move = [CCMoveTo actionWithDuration:0.2f position:scrollPosition];
  [tileMap stopAllActions];
  [tileMap runAction:move];
}

After obtaining the center position of the screen, I modify the tilePos y coordinate, because tilemap coordinates are counted from top to bottom (see Figure 10-11), and screen coordinates increase from bottom up. In effect, I convert the tilePos y coordinate as if it were counted from bottom up. In addition, I subtract 1 from the map’s height to account for the fact that tile coordinates are counted from 0. In other words, if the map’s height were 10, only the tile coordinates 0 to 9 would be valid.

The tilemap sizes (which are in pixel coordinates) are converted to points just as shown earlier in Listing 10-3. This ensures that scrolling works on both standard-resolution and Retina-resolution devices.

Next, create the scrollPosition CGPoint, which will become the position the tilemap will be moved to. The first step is to multiply the tile coordinates with pointWidth and pointHeight, respectively. You may be wondering why I negate the tilePosInPixels coordinates. It’s simply because if I want the tiles to move from top right to bottom left, I have to move the tilemap down and to the left by decreasing the coordinates.

The next big block modifies the coordinates of the scrollPosition to center the tile on the screen’s center point. You also need to take into account the center of the tile itself, which is why half the tileSize is deducted from the screenCenter offset.

Using the Objective-C language’s MIN and MAX macros ensures that the scrollPosition is kept within the bounds of the tilemap so as to not reveal anything past the borders of the tilemap. MIN and MAX return the minimum and maximum values of their two parameters and are a more compact and readable solution than conditional assignments using if and else statements.

Finally, use a CCMoveTo action to scroll the tilemap node so that the touched tile is centered on the screen. The result is a tilemap that scrolls to the tile that you tap. You can use the same method to scroll to a tile of interest—for example, the player’s position.

Tip  As for the player character itself, the next chapter has an implementation of isometric tilemaps. You can apply the same principle to orthogonal tilemaps. And this cocos2D forum thread will get you started with pathfinding on orthogonal tilemaps, source code included: www.cocos2d-iphone.org/forum/topic/19463.

If you’re interested in a complete, working, ready-made solution for a hack-and-slash game on orthogonal tilemaps, including a great tutorial, I recommend looking at Nate Weiss’ Action-RPG Engine: www.iphonegamekit.com.

Summary

You should now have a fair understanding of what tilemaps are, and you should know how to work with the Tiled (Qt) Map Editor to create a tilemap with multiple layers and properties your game can use.

Loading and displaying a tilemap with cocos2d is a simple task that quickly grows in complexity when it comes to obtaining tile and object layers, modifying them, and reading their properties. You also learned how to determine the tile coordinate of a touch location and how to use tile coordinates to scroll the tilemap so that it centers the touched tile on the screen.

I even got you acquainted with custom drawing and a bit of OpenGL ES code to render the object layer rectangles on the tilemap for debugging purposes.

In the next chapter you’ll learn how to work with isometric tilemaps and what it takes to implement a player character moving over the isometric world.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.217.55