Adding Tiled object layer support to cocos2d

Forums Programming cocos2d support (graphics engine) Adding Tiled object layer support to cocos2d

This topic contains 16 replies, has 5 voices, and was last updated by  riq 4 years, 3 months ago.

Viewing 17 posts - 1 through 17 (of 17 total)
Author Posts
Author Posts
November 8, 2009 at 3:11 pm #218305

neophit
@neophit

Hello,

I am working on implementing support for objectgroups in TMXTiledMap and TMXXMLParser. I would like to get input from other devs who may find this useful and to help make the implementation as generic as possible so it can be easily adapted to any game. I fully intend to share this code and hope that it may someday make its way into cocos2d for everyone to use.

For those who don’t know, Tiled supports two types of layers: Tile and Object. Tile layer is already supported by cocos2d. Object layer creates an XML element called “objectgroup” with child elements called “object”. Each object has attributes for name, type, position, and size. Objects also have a child element called “Properties” which has child elements called “Property”. Each Property has an attribute for name and value.

Here is an example of what I’ve just described:

<objectgroup name="SpawnPoints" width="10" height="10">
<object name="spawnPoint0" type="SpawnPoint" x="5" y="5" width="5" height="5"/>
<properties>
<property name="EnemyType" value="Monster"/>
<property name="SpawnRate" value="5"/>
</properties>
</object>
</objectgroup>

As you can see in the example, objects can be a useful way of creating things like spawn points, triggers, etc. Using the type attribute, objects could be created from cocos2d classes like CocosNode or AtlasSprite, or your own classes like EnemyMonster. The property values can be assigned to an object’s properties of the same name. In the example above, I have an object named spawnPoint0 which is of the class SpawnPoint. The value of the SpawnRate property would be assigned to the object similarly to spawnPoint0.SpawnRate = 5;.

Objects could be accessed like to tiles using something like this:

TMXTiledMap *map = [TMXTiledMap tiledMapWithTMXFile:@"orthogonal-test2.tmx"];
TMXObjectGroup *objects = [map groupNamed:@"SpawnPoints"];
SpawnPoint *spawnPoint0 = [objects objectNamed:@"spawnPoint0"]

I’ve begun adding the ability to parse objectgroups to TMXXMLParse by creating TMXObjectGroupInfo. This stores the name of the objectgroup and has an array of objects within that group.

/* TMXObjectGroupInfo contains the information about the objects like:
- ObjectGroup name

This information is obtained from the TMX file.
*/
@interface TMXObjectGroupInfo : NSObject
{
NSString *name_;
NSMutableArray *objects_;
}

@property (nonatomic,readwrite,retain) NSString *name;
@property (nonatomic,readwrite,retain) NSMutableArray *objects;

@end

To parse the objectgroups, I added this to the parser method:

} else if([elementName isEqualToString:@"objectgroup"]) {
TMXObjectGroupInfo *objectgroup = [TMXObjectGroupInfo new];
objectgroup.name = [attributeDict valueForKey:@"name"];

[objectgroups_ addObject:objectgroup];
[objectgroup release];

The object element is parsed and the resulting object is added to objectgroups_.objects.

} else if([elementName isEqualToString:@"object"]) {

TMXObjectGroupInfo *objectgroup = [objectgroups_ lastObject];
Class classFromString = NSClassFromString([attributeDict valueForKey:@"type"]);

if ( classFromString != nil )
{
id objectInstance = [[classFromString alloc] init];

Here I get a reference to the objectgroup then check to see if a class exists with the name specified in the type attribute. If the class does exist, I create an instance of an object with that class. Next, I need to assign the name, type, x, y, width, and height attributes to properties of the object. Since I know the types of these properties, I can use setValue:forKey to assign the appropriate values to each property. Size and Width are both structs which must be wrapped in NSValue before they can be sent to the receiver object. Key-Value coding handles unwrapping of NSValue automatically when using setValue:forKey. I am using try{} catch{} here because NSUndefinedKeyException is thrown if the object does not contain the key specified when using setValue:forKey. So if the class specified in type does not have a name property an exception is thrown.

@try {
[objectInstance setValue:[attributeDict valueForKey:@"name"] forKey:@"name"];
}
@catch (NSException * e) {
if ( [[e name] isEqualToString:NSUndefinedKeyException] )
NSLog(@"TMX tile map: %@ does not recognize the property "name"", objectInstance);
}

@try {
CGPoint objectPosition = CGPointMake([[attributeDict valueForKey:@"x"] floatValue], [[attributeDict valueForKey:@"y"] floatValue]);
[objectInstance setValue:[NSValue valueWithCGPoint:objectPosition] forKey:@"position"];
}
@catch (NSException * e) {
if ( [[e name] isEqualToString:NSUndefinedKeyException] )
NSLog(@"TMX tile map: %@ does not recognize the property "position"", objectInstance);
}

@try {
CGPoint objectSize = CGPointMake([[attributeDict valueForKey:@"width"] floatValue], [[attributeDict valueForKey:@"height"] floatValue]);
[objectInstance setValue:[NSValue valueWithCGPoint:objectSize] forKey:@"size"];
}
@catch (NSException * e) {
if ( [[e name] isEqualToString:NSUndefinedKeyException] )
NSLog(@"TMX tile map: %@ does not recognize the property "size"", objectInstance);
}

An alternative to catching the exception would be to use NSSelectorFromString to generate a selector from the key name then check if respondsToSelector: is true. This works with properties which have been @synthesize’ed since the accessor methods are created for us.

if ( [objectInstance respondsToSelector: [NSSelectorFromString( [attributeDict valueForKey:@"name"] )]] ) {
[objectInstance setValue:[attributeDict valueForKey:@"name"] forKey:@"name"];
}

I think this method makes cleaner code, but I’m not sure which would be more efficient.

Parsing the property elements is where things get tricky. Since the properties have only two attributes: name and value; we don’t know the type of data contained in value. It could be a string, int, CGPoint, CGSize, etc.

} else if([elementName isEqualToString:@"property"]) {

TMXObjectGroupInfo *objectGroup = [objectgroups_ lastObject];
id objectInstance = [objectGroup.objects lastObject];

NSString *propertyName = [attributeDict valueForKey:@"name"];
NSString *propertyValue = [attributeDict valueForKey:@"value"];

@try {
[objectInstance setValue:propertyValue forKey:propertyName];
}
@catch (NSException * e) {
if ( [[e name] isEqualToString:NSUndefinedKeyException] )
NSLog(@"TMX tile map: %@ does not recognize the property "%@"", objectInstance, propertyName);
}
[propertyName release];
[propertyValue release];
[objectInstance release];
[objectGroup release];
}

The above code should work for some types like NSString, but not CGPoint, CGSize, etc. One possible solution would be to check the object’s property type at runtime and convert the string value to the appropriate type. For example, if the property is a CGPoint we can use CGPointFromString to convert a value of {x,y} to a CGPoint. To determine a property’s type at runtime you must include <objc/runtime.h> and use the following code:

objc_property_t theProperty = class_getProperty(objectClass, [@"position" UTF8String]);

const char * propertyAttrs = property_getAttributes(theProperty);

If objectClass’s position property is a CGPoint then propertyAttrs would contain something like this: “T{CGPoint=ff},N,Vposition_”. This string is a type encoding. It can be parsed and used in a switch to convert the string value the appropriate type.

I am new to objective-c and cocos2d so please be gentle in your responses. :)

November 9, 2009 at 12:16 pm #265857

neophit
@neophit

I just finished working on parsing Properties and assigning them to the object. The following will parse each Property element, determine if the object has a property of the same name, and if so, assign the value to the object’s property. To accomplish this, I use the methods described in the post above to get the @encode type string for a @property defined in your object’s class. Then I search the @encode type string for keywords such as CGPoint, CGRect, int, float, etc. If it matches I convert the value string from the XML Property element to the appropriate type and set the value on the object.

For CGPoint, CGRect, and CGSize, the Property value in Tiled must be properly formatted in order for the connivence methods to convert the strings. If the value is not properly formatted, then CGPointZero, CGRectZero, or CGSizeZero will be returned. I included the proper formatting in the comments.

Additional types can easily be supported by determining its @encode type string and adding an else-if statement with a proper string conversion method.

} else if([elementName isEqualToString:@"property"]) {

TMXObjectGroupInfo *objectGroup = [objectgroups_ lastObject];
id objectInstance = [objectGroup.objects lastObject];

NSString *propertyName = [attributeDict valueForKey:@"name"];
NSString *propertyValue = [attributeDict valueForKey:@"value"];

if ( [objectInstance respondsToSelector:NSSelectorFromString(propertyName)] ) {

objc_property_t objectProperty = class_getProperty([objectInstance class], [propertyName UTF8String]);
NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(objectProperty)];

CCLOG(@"TMX tile map: Setting property: %@.%@ = %@", [objectInstance valueForKey:@"name"], propertyName, propertyValue );

if ([propertyAttributes rangeOfString:@"CGRect" options:NSLiteralSearch].location != NSNotFound) {

// CGRect: {{x,y},{w,h}}
[objectInstance setValue:[NSValue valueWithCGRect:CGRectFromString(propertyValue)] forKey:propertyName];

} else if ([propertyAttributes rangeOfString:@"CGPoint" options:NSLiteralSearch].location != NSNotFound) {

// CGPoint: {x,y}
[objectInstance setValue:[NSValue valueWithCGPoint:CGPointFromString(propertyValue)] forKey:propertyName];

} else if ([propertyAttributes rangeOfString:@"CGSize" options:NSLiteralSearch].location != NSNotFound) {

// CGSize: {w,h}
[objectInstance setValue:[NSValue valueWithCGSize:CGSizeFromString(propertyValue)] forKey:propertyName];

} else if ([propertyAttributes rangeOfString:@"NSString" options:NSLiteralSearch].location != NSNotFound) {

// NSString
[objectInstance setValue:propertyValue forKey:propertyName];

} else if ([propertyAttributes rangeOfString:@"float" options:NSLiteralSearch].location != NSNotFound) {

// float
[objectInstance setValue:[NSNumber numberWithFloat:[propertyValue floatValue]] forKey:propertyName];

} else if ([propertyAttributes rangeOfString:@"int" options:NSLiteralSearch].location != NSNotFound) {

// int
[objectInstance setValue:[NSNumber numberWithInt:[propertyValue intValue]] forKey:propertyName];

}

} else {
CCLOG(@"TMX tile map: Object '%@' does not recognize the property '%@'", [objectInstance valueForKey:@"name"], propertyName);
}
}

Next, I will be working on making these objects accessible via TMXTiledMap. I would greatly appreciate anyone’s feedback!

November 9, 2009 at 1:12 pm #265858

mhussa
@mhussa

Adding access to the tiled properties is a very good extension to the existing TMX code. Kudos for adding it.

One thing though, Using the type encoding as the value in the property name value pair is nice for iphone targets but not for others.

I am porting a game from my past(windows) to the iphone and the data I use is in tiled XML non-platform specific format. If I use this functionality it would mean adding platform specific property data which (going on past experience) I’m not going to do. I keep the data generic and make the platform read the data. It could be argued that you can parse the obj-c encoding on “other” platforms but then you’d need to sort out some alternative type to read the object into (e.g. CGpoint).

My preference is to keep it simple in the map data and have a simple encoding in the value attribute, text or number, nothing else.

November 9, 2009 at 1:54 pm #265859

neophit
@neophit

mhussa, thanks for the feedback. I’m not actually storing the type encoding in the XML Property value field. I’m looking at an instance of an object created using the class specified in the “type” attribute and detecting what type of data should be stored in a particular property. Let me show an example of how this works.

Here is an object represented in XML created in Tiled:

<object name="spawnPoint0" type="SpawnPoint" x="14" y="48" width="5" height="5">
<properties>
<property name="spawnRate" value="5"/>
<property name="spawnOffset" value="{5,3}"/>
</properties>
</object>

Note the braces {} around the numbers in spawnOffset. These are required by the convenience method CGPointFromString. It would be trivial to add them before calling setValue if people preferred 5,3 over {5,3}.

Here is the SpawnPoint class defined in code:

#import "cocos2d.h"

@interface SpawnPoint : CocosNode {
NSString *name;
int spawnRate;
CGPoint spawnOffset;
}
@property (nonatomic,readwrite,retain) NSString *name;
@property (nonatomic,readwrite,assign) int SpawnRate;
@property (nonatomic,readwrite,assign) CGPoint SpawnOffset;
@end

@implementation SpawnPoint
@synthesize name, spawnRate, spawnOffset;

-(id) init
{
if( (self=[super init]) ) {
self.name = nil;
self.spawnRate = 1;
self.spawnOffset = CGPointZero;
}
return self;
}

-(void) dealloc
{
[name release];
[super dealloc];
}

@end

When I parse the XML property elements, I check to see if the object has a @property matching the name attribute. If it does, I get use property_getAttributes to determine the data type stored in the @property. That method returns an @encoder string which I then compare in the else-if statements to see if I can convert the string to the appropriate data type.

November 9, 2009 at 2:08 pm #265860

mhussa
@mhussa

OK I understand, automagically matching the tiled property to the obj-c property is very good. As long as the tiled value strings stay relatively simple to parse its even better.

I personally would have xoffset and yoffset instead to ensure each name=value pair had a single value. A bit more inefficient but better for portability (for me). What you have so far is fine. The developer can decide whether to have values with structure (2 values) if they wish.

I might be able to use something like this.

Thanks.

November 9, 2009 at 2:13 pm #265861

neophit
@neophit

I just noticed that Tiled stores Layer properties using the same tags. I was only planning on adding objectgroup, object, and object property support to cocos2d, but I might as well tackle Layer Property and ObjectGroup Property since they’re being parsed by my code anyway.

The simplest solution I can think of right now is to add an NSDictionary to TMXLayerInfo and TMXObjectGroupInfo to store each property as a key-value pair. Then pass that dictionary on to TMXTiledMap. The properties could be accessed directly via the NSDictionary or via a convenience method like (id)propertyNamed(NSString *)propertyName.

Thoughts?

November 9, 2009 at 3:08 pm #265862

mhussa
@mhussa

I prefer a propertyName convenience method because I don’t care how its stored I just want the value string for a given property name.

You should raise an issue to get this into the codebase (see wiki).

Thanks.

November 9, 2009 at 3:43 pm #265863

mhussa
@mhussa

I have one more question though. How would I access the x,y,width and height of an object?

November 10, 2009 at 12:11 am #265864

neophit
@neophit

Since all objects in cocos2d are based on CocosNode, I’m assigning the x, y values to position and the width, height values to contentSize. So you can access those values as you would normally access their properties.

spawnPoint.position

spawnPoint.position.x

spawnPoint.position.y

spawnPoint.contentSize

spawnPoint.contentSize.width

spawnPoint.contentSize.height

Here is how I parse those values from XML and assign them to the objects’ properties.

if ( [objectInstance respondsToSelector:NSSelectorFromString(@"position")] ) {
CGPoint objectPosition;
objectPosition.x = [[attributeDict valueForKey:@"x"] intValue];
objectPosition.y = [[attributeDict valueForKey:@"y"] intValue];
[objectInstance setValue:[NSValue valueWithCGPoint:objectPosition] forKey:@"position"];
} else
CCLOG(@"TMX tile map: Object '%@' does not recognize the property 'position'", objectInstance);

if ( [objectInstance respondsToSelector:NSSelectorFromString(@"contentSize")] ) {
CGSize objectSize;
objectSize.width = [[attributeDict valueForKey:@"width"] intValue];
objectSize.height = [[attributeDict valueForKey:@"height"] intValue];
[objectInstance setValue:[NSValue valueWithCGSize:objectSize] forKey:@"contentSize"];
} else
CCLOG(@"TMX tile map: Object '%@' does not recognize the property 'contentSize'", objectInstance);

November 10, 2009 at 2:02 am #265865

mhussa
@mhussa

In your example I’m not sure a spawnpoint is a valid cocosnode object. A lot of the cocosnode functionality is really relevant to visible objects which can use camera, gridbase, rotation etc.

For setting up a visible (cocosnode) object its neat. If I want to setup a Sprite it’ll work because it has the position property.

How would it be used for non-cocosnode game objects?

I use plain objects to represent game objects which use world co-ordinates for testing not view co-ordinates. The game object definition is “composed” (not subclassed) of things like sprites for rendering the actual object.

Its a tricky one. Maybe you can keep this setup for people wanting to create cocosnode game objects and provide some other functions into TMXObjectGroup that allow this data to be queried.

The “create an object” approach is nice but if its not in keeping with the current codebase it may be better to stick with a property/method accessor approach.

Anybody else got any views?

November 10, 2009 at 3:52 am #265866

neophit
@neophit

The objects created in Tiled can use any objective-c or cocos2d object class in the Type field. The class does not have to be a subclass of CocosNode. If you put NSObject, then the parser will create an instance of the NSObject class. Since NSObject does not have position or contentSize those values will not be used.

If you want to create an object which is not drawn, but still has position, size, or other data. You could add a position and contentSize property to your object which will use the x, y, width, and height attributes provided by Tiled. Or you could add properties to your object in Tiled with the data you want to access.

November 21, 2009 at 8:04 pm #265867

neophit
@neophit

It is finally complete! I have implemented support for objectgroups, objects, and properties in TMX tile maps. Once I have finished adding some more details to the doxygen comments, I will create an issue on google code and post my patch. It should be ready later today.

November 21, 2009 at 10:51 pm #265868

neophit
@neophit

Issue 636 has been created. If you want to test out the changes I’ve made, apply the patches attached to the issue.

November 22, 2009 at 3:35 am #265869

bradparks
Participant
@bradparks

thanks alot for posting this… your work and contribution back to the project is greatly appreciated!

December 23, 2009 at 11:57 am #265870

mr_zaz
@mr_zaz

Great Work , I see this has made it into the latest 0.9 branch , are there plans to update the TileMap ‘tests’ to use this feature ?

December 23, 2009 at 7:54 pm #265871

neophit
@neophit

I can create some tests later this week or maybe next week.

December 23, 2009 at 8:03 pm #265872

riq
Keymaster
@admin

@neophit: thanks. Tests are very welcomed. If you code the tests, could you add them to the issue tracker ? Thanks.

Viewing 17 posts - 1 through 17 (of 17 total)

You must be logged in to reply to this topic.