More on code sharing
I wrote the other day about Sharing code in iPhone applications which was about using static libraries. I ran into an interesting problem on the weekend where by my code was working on the simulator but not on real hardware.
In my static library I was using kissxml to parse a tile map from Tiled. The TiledMap code was from the guys at 71Squared.
In the simulator things worked as expected. On an iPhone/iPod the program would always crash trying to parse the XML. The program was aborting because NSStringAdditions were not taking effect. The specific error was [NSCFString xmlChar]: unrecognized selector sent to instance
If I had all the code in a single program it worked and as I mentiond there were no problems with library + app in the simulator. It was very odd and unfortunately I was unable to determine how to fix this.
As a workaround, I re-wrote the TiledMap class to use NSXMLParser instead of kissxml. It’s a bit slower than kissxml but at least it works in a static library on the simulator and devices. Only difference from original code is that it doesn’t support layer properties yet.
The original code from 71Squared was released under the MIT license. Here are my changes under the same license.
Long term, the ideal situation would be for Apple to add some XML APIs that can handle XPath in a future OS release.
TiledMap.h
- (id)initWithTMXFile:(NSString *)tmxFile;
TiledMap.m
- (id)initWithTMXFile:(NSString *)tmxFile {
if ((self = [super init])) {
// Shared game state
sharedDirector = [Director sharedDirector];
// Set up the default colour filter
colourFilter = Color4fInit;
// Set up the arrays and default values for layers and tilesets
tileSets = [[NSMutableArray alloc] init];
layers = [[NSMutableArray alloc] init];
// Allocate and init the properties dictionary for the map
mapProperties = [[NSMutableDictionary alloc] init];
// Init the current layer, tileset and tile x and y
currentLayerID = 0;
currentTileSetID = 0;
tileX = 0;
tileY = 0;
NSURL *url = [NSURL fileURLWithPath: [[NSBundle mainBundle] pathForResource:tmxFile ofType:@"tmx"]];
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
[parser setDelegate:self];
[parser setShouldProcessNamespaces:NO];
[parser setShouldReportNamespacePrefixes:NO];
[parser setShouldResolveExternalEntities:NO];
[parser parse];
NSError *error = [parser parserError];
if (error) {
NSLog(@"Error parsing TMX file: %@", error);
}
[parser release];
// Calculate the total number of tiles it would take to fill the visible screen. The values below which define the screen
// size would need to be changed based on the size of the area you need the tilemap to fill. I am adding a couple of tiles
// to the total to cover fractions of a tile which may have resulted in the calculation. I am then multuplying the result
// by two as there are two triangles per tile
int totalTriangles = ((320 / tileWidth) + 2) * ((480 / tileHeight) + 2) * 12 ;
if(DEBUG) NSLog(@"--> Initializing vertex arrays for '%d' triangles.", totalTriangles);
// Set up the vertex arrays
tileVerts = calloc(totalTriangles, sizeof(TileVert));
// If one of the arrays cannot be allocated, then report a warning and return nil
if(!tileVerts) {
if(DEBUG) NSLog(@"WARNING: Tiled - Not enough memory to allocate vertex arrays");
return nil;
}
}
return self;
}
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
if([elementName isEqualToString:@"map"]) {
mapWidth = [[attributeDict valueForKey:@"width"] intValue];
mapHeight = [[attributeDict valueForKey:@"height"] intValue];
tileWidth = [[attributeDict valueForKey:@"tilewidth"] intValue];
tileHeight = [[attributeDict valueForKey:@"tileheight"] intValue];
} else if([elementName isEqualToString:@"tileset"]) {
tileSetName = [[attributeDict valueForKey:@"name"] retain];
tileSetFirstGID = [[attributeDict valueForKey:@"firstgid"] intValue];
tileSetSpacing = [[attributeDict valueForKey:@"spacing"] intValue];
// tileset->margin = [[attributeDict valueForKey:@"margin"] intValue];
tileSetWidth = [[attributeDict valueForKey:@"tilewidth"] intValue];
tileSetHeight = [[attributeDict valueForKey:@"tileheight"] intValue];
tileSetProperties = [[NSMutableDictionary alloc] init];
} else if([elementName isEqualToString:@"layer"]) {
layerName = [[attributeDict valueForKey:@"name"] retain];
layerWidth = [[attributeDict valueForKey:@"width"] intValue];
layerHeight = [[attributeDict valueForKey:@"height"] intValue];
currentLayer = [[Layer alloc] initWithName:layerName layerID:currentLayerID layerWidth:layerWidth layerHeight:layerHeight];
if(DEBUG) NSLog(@"--> LAYER found called: %@, width=%d, height=%d", layerName, layerWidth, layerHeight);
[layers addObject:currentLayer];
[currentLayer release];
currentLayerID++;
} else if([elementName isEqualToString:@"image"]) {
NSString *source = [[attributeDict valueForKey:@"source"] retain];
// Create a tileset instance based on the retrieved information
currentTileSet = [[TileSet alloc] initWithImageNamed:source
name:tileSetName
tileSetID:currentTileSetID
firstGID:tileSetFirstGID
tileWidth:tileSetWidth
tileHeight:tileSetHeight
spacing:tileSetSpacing];
// Add the tileset instance we have just created to the array of tilesets
[tileSets addObject:currentTileSet];
// Release the current tileset instance as its been added to the array and we do not need it now
[currentTileSet release];
// Increment the current tileset id
currentTileSetID++;
} else if([elementName isEqualToString:@"tile"]) {
if ([attributeDict valueForKey:@"id"] != nil) {
currentTileID = [[attributeDict valueForKey:@"id"] intValue] + tileSetFirstGID;
} else {
Layer *layer = [layers lastObject];
int globalID = [[attributeDict valueForKey:@"gid"] intValue];
// If the globalID is 0 then this is an empty tile else populate the tile array with the
// retrieved tile information
if(globalID == 0) {
[layer addTileAtX:tileX y:tileY tileSetID:-1 tileID:0 globalID:0];
} else {
TileSet *tileSet = [self findTileSetWithGlobalID:globalID];
[layer addTileAtX:tileX
y:tileY
tileSetID:[tileSet tileSetID]
tileID:globalID - [tileSet firstGID]
globalID:globalID];
}
// Calculate the next coord within the tiledata array
tileX++;
if(tileX > layerWidth - 1) {
tileX = 0;
tileY++;
}
}
} else if([elementName isEqualToString:@"property"]) {
NSString *tileIDKey = [NSString stringWithFormat:@"%d", currentTileID];
NSMutableDictionary *tileProperties = [[NSMutableDictionary alloc] init];
NSString *name = [attributeDict valueForKey:@"name"];
NSString *value = [attributeDict valueForKey:@"value"];
if(DEBUG) NSLog(@"----> Property '%@' found with value '%@' for global tile id '%@'", name, value, tileIDKey);
[tileProperties setObject:value forKey:name];
[tileSetProperties setObject:tileProperties forKey:tileIDKey];
// Release the tileProperties now they have been added to tileSetProperties
[tileProperties release];
}
}