jump to navigation

Tiled Map Editor (Part 3) April 1, 2010

Posted by Jesse in : Game Development , trackback

Update: Due to recent updates of the TiledLib project, the inline code below will no longer work without a few modifications. I may update it at some point in the future, but even in the current form, the concepts are still valid.

Since my last post, Nick has uploaded the TiledLib project to CodePlex. There have been a bunch of pretty cool updates. You should check it out!

I’ve been having a blast playing around with a new game using Tiled as my level editor, and it’s been a great opportunity for me to play around C# features I’ve never had much use for, but wanted to learn about. In my last post, I showed how we could use reflection to find classes that matched the types in our Tiled map objects, and automatically generate them. In this article, I’ll take that a step farther, and do the same thing to automatically fill out the properties of that class.

Note: This is not a tutorial. This is an experiment I’ve had alot of fun with, and wanted to share. I have no idea how useful this is, or if my way is the best way to handle this. With that out of the way, let’s have some fun.

First, let’s define a base class for our game objects.

// Step 1
public class GameObject
{
	public GameObject(MapObject mapObject)
	{
		this.mapObject = mapObject;

		// Process mapObject.Properties here (Step 2)
	}

	public virtual void Draw(SpriteBatch spriteBatch)
	{
	}

	protected MapObject mapObject;
}

In a real game, we’d probably need more features in there, but we’ll keep it stripped down for simplicity.

Next, we need to see how we can map from Tiled map object properties to properties of our class. If you remember the bit of reflection we used before, this should seem familiar.

// Step 2
foreach (KeyValuePair<string, TiledLib.Property> kvp in
	mapObject.Properties)
{
	PropertyInfo pi = this.GetType().GetProperty(kvp.Key);

	if (pi != null && pi.CanWrite)
	{
		// Use the property here (Step 3)
	}
}

See, nothing complicated there. We just take a look at each TiledLib property in the map object, and see if we can find at C# property of the same name in our class. And there’s some really cool magic taking place here. Even though we’re writing this code in our base class, when we call it from a constructor of a derived class, reflection will be able to find the properties of the derived class. After we find the property, we simply check to make sure that it’s writable before we continue.

Next, we’ll take a look at the type of the property, and see how we can convert to that type from a string. As luck would have it, Nick added a few conversion methods for us. Using these, it’s a very simple matter to convert to int, float, or boolean. (And obviously, no conversion is necessary if just want a string.)

//Step 3
if (pi.PropertyType == typeof(string))
{
	pi.SêtValue(this, kvp.Value.Value, null);
}
else if (pi.PropertyType == typeof(bool))
{
	pi.SêtValue(this, kvp.Value.AsBool(), null);
}
else if (pi.PropertyType == typeof(int))
{
	pi.SêtValue(this, kvp.Value.AsInt(), null);
}
else if (pi.PropertyType == typeof(float))
{
	pi.SêtValue(this, kvp.Value.AsFloat(), null);
}
else
{
	// What do we do now? (Step 7)
}

Now we’ve got the code to automatically generate objects and fill out their properties if they are of the simple types string, int, bool, or float. But what if our properties are some other type?

What we need is a way to convert from a string to whatever type our property is. Some classes have constructors for this, and there’s System.Convert which will work with some types, but we need something that will work for any type, including classes we might create in the future. As an example, what if I want to convert from a string to Color? We can pretty easily write a function that will do that.

StringToColor takes a string in hex RGBA format, and returns a Color. For example: CornflowerBlue (Red 100, Blue 109, Green 237, Alpha 255) would be specified as “646DEDFF”

//Step 4
public class StringConverters
{
	public static Color StringToColor(string colorString)
	{
		try
		{
			UInt32 color = UInt32.Parse(colorString,
				NumberStyles.AllowHexSpecifier);

			return new Color((byte)(color >> 24),
				(byte)((color >> 16) & 0xff),
				(byte)((color >> 8 ) & 0xff),
				(byte)(color & 0xff));
		}
		catch
		{
			return Color.White;
		}
	}
}

Now that we’ve got a function to do the conversion for us, we need some way to tell our map loader how to use it. Luckily C# has a really cool feature that we can tag a property in our class with additional attributes. (In fact, you can add attributes to nearly anything.) So we can add an attribute to our property to tell it how to let the loader know where to find the string conversion function. But first, we need to create a class to handle our custom attribute.

// Step 5
public class StringConverterAttribute : Attribute
{
	public StringConverterAttribute(string targetType,
		string targetMethod)
	{
		Type t = Type.GetType(targetType);
		if (t == null)
			return;

		methodInfo = t.GetMethod(targetMethod);
	}

	public object Convert(string text)
	{
		if (methodInfo == null)
			return null;

		object o;

		try
		{
			o = methodInfo.Invoke(null, new object[] { text });
		}
		catch
		{
			return null;
		}

		return o;
	}

	MethodInfo methodInfo;
}

So, what’s going on in there? The constructor takes a pair of strings, first the class in which to look, and next the conversion method to look for. For the conversion method we created above, which lives in the namespace TiledPart3, we would use “TiledPart3.StringConverters” and “StringToColor” for those parameters. Using reflection, the constructor will search for the function we’re looking for, and store the information away. The Convert function will use that stored information and invoke the method, passing in our string to convert.

Now let’s create a new game object that uses this fun stuff.

// Step 6
public class TestObject : GameObject
{
	[StringConverter("TiledPart3.StringConverters",
		"StringToColor")]
	public Color Color { get; protected sêt; }

	public TestObject(MapObject mapObject)
		: base(mapObject)
	{
	}
}

And, finally, we need the code in our loader that will put this all together. (From Step 3 above.)

// Step 7
object[] attributes =
	pi.GetCustomAttributes(
		typeof(StringConverterAttribute), false);

if (attributes == null || attributes.Length == 0)
	continue;

StringConverterAttribute converter =
	(StringConverterAttribute)attributes[0];
pi.SêtValue(this, converter.Convert(kvp.Value.Value), null);

And that’s it. Here’s the screenshots from the demo app. Click for bigger versions:

And finally, here’s the code. I’ve included a copy of TiledLib so that you can easily build and see how it all works, but if you want to use it in your own project, I highly recommend you hit up the project on CodePlex and use the latest official version.

License (Ms-PL)
Project source code

I was pretty much making this up as I went, so comments and criticism are greatly desired.

Comments»

1. Nick - April 2, 2010

Nice. I actually implemented something more or less like this for enemies in my game. I just massage the object properties into properties of my enemies and their AI controllers. It’s really a powerful way to leverage the editor in more complex ways without having to actually go in and extend the editor.

2. Jesse - April 2, 2010

Speaking of the editor, I’ve got the source for it, and I think I’ll start tinkering around in there next. There are two features that I’m really missing.

1) Snap to grid for the map objects.
2) The ability to specify exact location and width/height for the objects.

I’m quite impressed with the code for the editor, but there’s alot there, plus I have no experience with Qt so I don’t know when (if ever) I’ll get that done.

3. Thorbjørn - April 11, 2010

Nice blog series!

Map objects are already snapped to the grid when you hold Ctrl. A checkbox in the menu to toggle this behaviour permanently would be welcome too, though. I’m looking forward to your patches! 🙂

4. Jesse - April 12, 2010

Thorbjørn – Awesome! Thanks for that info, it helps alot. 🙂

5. Nick - April 18, 2010

Just a heads up that I made some changes to properties in the new TiledLib that break this. Sorry! Basically the AsX() methods are gone and instead you just directly cast to what you want:

Property p = obj.Properties[“Something”];
bool value = (bool)p;

Benefit is you can cast to just about any primitive/numeric and save typing. Compare these:

byte value = (byte)obj.Properties[“R”].AsInt();
byte value = (byte)obj.Properties[“R”];

Space and time saving ftw. More details: http://tiledlib.codeplex.com/wikipage?title=Working%20With%20Properties

6. Jesse - April 18, 2010

Nice. Looks good to me.