2.2. Working with Maps Document

Previous: Quickstart Guide - Next: Working with LayersReturn to Index

Maps are represented by the view (node) class TKMapNode and its model class TKMap. TKMapNode holds an instance of TKMap in its map property.

Working with TKMapNode

There’s two main ways of creating a map node.

Load a TMX file

To create a TKMapNode from a TMX file:

TKMapNode* mapNode = [TKMapNode mapNodeWithContentsOfFile:@"worldmap.tmx" renderSize:CGSizeZero];

// Swift
let mapNode = TKMapNode(contentsOfFile:"worldmap.tmx", renderSize:CGSizeZero)

Passing a TKMap instance

You can also create a TKMapNode based on an already-existing (perhaps randomly generated) TKMap instance:

TKMap* map = [TKMap mapWithContentsOfFile:@"map.tmx"];
TKMapNode* mapNode = [TKMapNode mapNodeWithMap:map renderSize:CGSizeZero];

// Swift
let map = TKMap("map.tmx")
let mapNode = TKMapNode(map:map, renderSize:CGSizeZero)

What’s RenderSize?

With a renderSize of CGSizeZero all tiles in the map will be drawn, all tiles will have a sprite node associated with them. If the map is larger than the screen, this quickly becomes inefficient. Hence the renderSize parameter.

Most often you’ll want large tilemaps to only process visible tiles, and to only reserve memory for the visible tiles. In that case you’ll simply use the view’s size as the renderSize. But in general any renderSize is possible, for instance if the tilemap only takes up three quarters of the screen width because of the GUI in the remaining quarter.

When you specify a custom renderSize, the map will stop drawing tiles outside the border rectangle defined by the map’s position and the renderSize. There’s an additional one or two rows and columns drawn to allow for smooth scrolling.

In the example screenshots below, if the map were to scroll downwards, there would already be enough tiles coming in from the top to ensure there aren’t any “holes” as tiles become partially visible. This overhead is two rows/columns for isometric and hexagonal maps, and one row/column for orthogonal maps.

renderSize = CGSizeZero renderSize = CGSizeMake(200, 140)

To use a screen-sized renderSize:

CGSize viewSize = tk_getViewSize(self);
TKMapNode* mapNode = [TKMapNode mapNodeWithContentsOfFile:@"worldmap.tmx" renderSize:viewSize];

// Swift
let viewSize = tk_getViewSize(self)
let mapNode = TKMapNode(contentsOfFile:"worldmap.tmx", renderSize:viewSize)

The tk_getViewSize(self) function returns the size of the view regardless of whether you’re using SpriteKit or Cocos2D.

Tip: Always specify the view’s size as the renderSize if your tilemap is larger than the screen. Otherwise you’d be wasting performance and memory.

Accessing Layer Nodes

You can simply enumerate the map node’s children to enumerate the map’s TKLayerNode instances:

for (TKLayerNode* layer in mapNode.children) {

// Swift
for layer in mapNode.children {

You will have to cast the TKLayerNode instances accordingly. Or alternatively, enumerate one of tileLayerNodes, objectLayerNodes or imageLayerNodes to enumerate only the layers of a specific type.

Lastly, you can get a layer node by its name:

TKTileLayerNode* gameLayerNode = (TKTileLayerNode*)[mapNode layerNodeNamed:@"Game Layer"];

// Swift
let gameLayerNode = mapNode.layerNodeNamed("Game Layer") as! TKTileLayerNode

Tip: You may want to cache commonly used layers in a property or ivar to avoid the enumeration / name lookup overhead.

Fixing Rendering Artifacts

When scaling a TKMapNode, or changing one of its parent’s positions, so-called “line artifacts” may become visible. These are gaps in the seams between tiles that occur due to floating point rounding inaccuracies. They can occur in both horizontal or vertical direction. Here’s an example that occured while scaling the map:

There are traditionally two ways to cure such artifacts:

  • Ensure the layer, map and their parent nodes are aligned on an exact pixel coordinate (ie {10.5, 25.0} rather than {10.4999998, 24.999997}).
  • Overlap the tiles slightly to cover any remaining seams, specifically when the cause of the artifacts is a scale factor that’s not a multiple of 2.

In TilemapKit you can apply both fixes easily.

Note: Other “line artifacts” can also occur if margin and spacing of the tileset are too small or zero, specifically when TKMapNode’s textureFilteringEnabled property is set to YES (not recommended). A margin/spacing of 2 pixels for tileset images is generally recommended to prevent these rendering artifacts.

Ensuring Pixel-Perfect Position

Whenever you move the map or one of its layers and don’t take precautions to ensure the positions are on pixel boundaries, you may experience “line artifacts”.

The fix is quite simple: just call TKMapNode’s clampPositionToNearestPixel every time the map or its layers have changed their position. In a scrolling game, where the layers change position practically every frame, simply run clampPositionToNearestPixel every update.

The tricky part here is to ensure clampPositionToNearestPixel runs after all movement occured, ie after the physics and action subsystems have been processed.

SpriteKit provides the didFinishUpdate method that is guaranteed to run after actions and physics. In Cocos2D you can schedule update methods with different priorities.

In cases where the map’s parent nodes may also change their positions, you can apply the pixel-boundary position fix upwards in the node hierarchy by calling TKMapNode’s clampPositionToNearestPixelRecursively instead.

Configuring Tile Overlap

The TKMapNode tileOverlap property allows you to configure how much (in points) tiles overlap each other. The tileOverlap is mainly useful to fix artifacts that occur when scaling or rotating a map.

The value 0.0 means no tile overlap. A positive value means tiles will be assumed to be the given amount (in points) larger than normal.

Typically setting tileOverlap to 0.5 should fix most remaining artifacts.

It is recommended to only enable tileOverlap while you run an operation (ie scaling or rotating map) that causes the artifacts. Because increasing the tile overlap can introduce other issues like slightly cut-off tiles due to the fact that tiles are now overlapping. Those artifacts are not normally visible while the map is moving, scaling or rotating, but may be noticable otherwise.

Multithreaded Rendering

By default multiThreadedRendering is enabled. This performs layer updates using a concurrent GCD queue. Depending on the map and the number of layers, as well as the device hardware, this will give you a nice performance boost.

Though you should know that on iOS the GPU becomes a bottleneck a lot earlier than the CPU does, so multithreading doesn’t provide a 2x/3x boost on iOS. It’s more like 10-30%.

You may want to temporarily disable multithreaded updates when you suspect multithreading issues.

Debug Drawing

You can set debugDrawOptions which are defined in the TKDebugDrawOptions enum. The options can be OR'ed together:

mapNode.debugDrawOptions = TKDebugDrawRenderArea | TKDebugDrawGrid | TKDebugDrawMouseOverTile;
mapNode.debugDrawEnabled = YES;

// Swift (hey, it's true, not YES!)
mapNode.debugDrawOptions = TKDebugDrawRenderArea | TKDebugDrawGrid | TKDebugDrawMouseOverTile
mapNode.debugDrawEnabled = true

After setting the options you can set debugDrawEnabled to YES which will create and add the nodes responsible for drawing the debug information.

Additionally, you can draw a GKGraph instance anchored on the map:

[mapNode drawGraph:graph];

// Swift (behold its magnificence!)
Note Some debugging options may be unavailable or untested, depending on whether you use SpriteKit or Cocos2D, and some options (mouse over) are more meaningful on OS X than on iOS. Debug drawing is mainly a remnant of TilemapKit development. The drawing output is subject to change. Furthermore, debug drawing is implemented to be correct and functional first and foremost, performance can degrade severely with specific or several debug drawing options enabled.

Working with TKMap

As you may have noticed, the things that TKMapNode does is mainly concerned about drawing layers. The actual code and data for the map is available in the TKMap class - it’s a strict “model” class as in the MVC terminology.

Accessing TKMap Instance

The TKMap instance of a TKMapNode is easy to access:

TKMap* map = mapNode.map;

// Swift
// Come on, do you really need to see this as Swift code as well? ... Okay, fine.
let map = mapNode.map

The TKMap class is the root of the model class hierarchy. It contains an array of TKLayer instances, and an array of TKTileset instances as well as the map’s TKProperties.

TKMap provides the Data

A large portion of the TKMap reference entails properties. The following list only includes properties that you will likely want to refer to or modify at runtime:

  • TKMap name (defaults to the TMX file name)
  • TKMap size (size of the map in tile coordinates) and TKMap sizeInPoints (same, but in points)
  • TKMap gridSize (size of individual tiles aka the grid)
  • TKMap orientation (type of map: orthogonal, isometric, staggered isometric, or hexagonal)
  • TKMap backgroundColor (hex color string)

Working with TKMap Properties

Like many other object, the map itself has an instance of TKProperties available via the TKMap properties @property.

Tip In Tiled, the map properties are a little hidden. You'll reveal them by choosing Map -> Map Properties to reveal the map's properties in the properties pane.

Get/Set Tile Properties

Via the map you also get quick access to tile properties, which are usually stored inside a tileset. But if you don’t know which tileset you’re looking for, you would do:

TKProperties* tileProperties = [map propertiesForTile:123];

// Swift
let tileProperties = map.propertiesForTile(123)

This returns the properties (if any) for the tile with the given global tile GID.

Tip If you frequently need a specific tile's properties, you should consider caching the properties or its values to avoid the lookup overhead.

You can also assign a TKProperties instance to a tile at runtime. If the tile already has a TKProperties instance, the new properties instance will replace the previous one.

[map setProperties:tileProperties forTile:234];

// Swift
map.setProperties(tileProperties, forTile:234)

Get Tiles with specific Property and Value

If you ever need a collection of tiles which have the same property, or which have the same property and matching value, you can do this as follows:

NSMutableArray tiles = [map tilesForPropertyNamed:@"aPropertyName" value:@10];

// Swift
let tiles = map.tilesForPropertyNamed("aPropertyName", value:10)

This will return a mutable array containing tile GIDs of the tiles which have a property named “aPropertyName” where the value is 10.

If you only wanted the tiles with the given property, regardless of the property’s value, then simply pass nil as the value parameter.

If you’re mostly interested in associating tile GIDs with a specific value stored in a property, then you can use:

NSMutableDictionary* tiles = [map tileValuesForPropertyNamed:@"pathCost"];

// Swift
let tiles = map.tileValuesForPropertyNamed("pathCost")

This will create and return a dictionary where the property values are stored as values, and the keys are tile GIDs as NSNumber.

Both methods are supposed to be used to obtain the necessary tile array or dictionary used by GameplayKit functions such as gridGraphForTileLayersNamed:walkableTiles:blockedTiles:diagonalsAllowed: and gridGraphForTileLayersNamed:costs:diagonalsAllowed:.

Working with Tilesets

A TKMap instance contains the map’s array of tilesets, which are instances of the TKTileset class.

Accessing Tilesets

Besides enumerating TKMap’s tilesets, you can also get a tileset by its name:

TKTileset* cityTileset = [map tilesetNamed:@"city"];

// Swift
let cityTileset = map.tilesetNamed("city")

Sometimes, you may need to know which tileset a tile GID belongs to. You can get the tileset associated with a tile GID by running:

TKTileset* tileset = [map tilesetForTile:123];

This will return the tile GID’s originating tileset. If you pass in an invalid tile GID (out of range, negative value) then nil is returned.

Adding/Removing Tilesets

At runtime, you can add or remove a tileset:

[map addTileset:myTileset];
[map removeTileset:oldTileset];

// Swift

Working with Layers

A TKMap instance also contains an array of layers which are represented by TKTileLayer, TKObjectLayer and TKImageLayer depending on the layer’s type. All TKLayer instances inherit from the abstract base class TKLayer.

Accessing Layers

You can get a layer by its name:

TKTileLayer* tileLayer = (TKTileLayer*)[map layerNamed:@"My Tile Layer"];

// Swift
let tileLayer = map.layerNamed("My Tile Layer") as! TKTileLayer

Adding/Removing Layers

To add or remove layers at runtime:

[map addLayer:layer];
[map insertLayer:layer atIndex:3];
[map removeLayer:layer];

// Swift
map.insertLayer(layer, atIndex:3)

Note: If you only need to hide a layer temporarily, it’s recommended to simply change the hidden/visible property of the TKLayerNode instance rather than removing and re-adding the layer (or layer node).

Accessing Global Tile Animations

The TKMap instance stores all global tile animationed contained in the TMX format.

You can add or replace an existing tile animation by creating a TKTileAnimation instance and at a minimum setting its tile property. Then call:

TKTileAnimation* animation = [TKTileAnimation new];
animation.tile = 123;
[map addTileAnimation:animation];

// Swift
let animation = TKTileAnimation()
animation.tile = 123

This will store the tile animation for its Tile GID. If there was a previously assigned tile animation with this tile GID, the new animation will replace the existing one.

To access this tile animation:

TKTileAnimation* animation = [map tileAnimationForTile:123];

// Swift
let animation = map.tileAnimationForTile(123)

If the tile GID has properties, and there’s a property named anim.name with a non-empty string, then you can also access the animation by its name (recommended since tile GIDs can change):

TKTileAnimation* animation = [map tileAnimationNamed:@"trapdoor"];

// Swift
let animation = map.tileAnimationForTile("trapdoor")

Previous: Quickstart Guide - Next: Working with LayersReturn to Index