I implemented some basic game mechanics for Astroboids and I was about to start working on a map editor. First thought was a tile based system that almost everybody seems to be using and then I found this article about an editor that “doesn’t use tiles but pieced images”, very nice stuff but it could take a while to implement. I then came across GLEED2D, “a non tile-based Level Editor for 2D games of any genre that allows arbitrary placement of textures and other primitive items in 2D space” according to the description on its website. GLEED2D is surprisingly good and easy to use, it saves XML, and of course, it beats having to write my own tool. The XML seems to be simply .Net objects serialized to XML, but hey, Java does XML and it looked like a few XPath expressions could take care of it.
Keep reading for a slightly “beyond the basics” tutorial and demo (it is the XML that makes things a little more complex, that’s all)
So above is what GLEED2D looks like and below is the XML that it generates:
Visible="true"> 330 70 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 455 280 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 325 225 0 1 255 255
255
2554294967295 false false Man.png Man 32 32 80 50 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 85 265 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 515 60 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 475 160 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 190 145 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 370 405 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 130 435 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 245 315 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 520 420 0 1 255 255
255
2554294967295 false false Spirit.png Spirit 32 32 1 1 13 C:\dev\workspace\GLEED2D Level\res 425.000641 225.0004
Now lets extract what we need out of this XML document. First, an utility class that hides some of the XPath’s ugliness. (for additional info on the “functors” that you see in the code refer to my previous posts here and the LazyCache class is explained here)
public class XmlUtil {
static XPathFactory factory = XPathFactory.newInstance();
static Map xpathExpressions = new LazyCache(64,
new Functor1() {
@Override
public XPathExpression invoke(String query) {
XPath xpath = factory.newXPath();
XPathExpression expr;
try {
expr = xpath.compile(query);
} catch (XPathExpressionException e) {
throw new RuntimeException(e);
}
return expr;
}
});
public static NodeList selectNodeList(Node doc, String query) throws XPathExpressionException {
XPathExpression expr = xpathExpressions.get(query);
return (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
}
public static Node selectNode(Node doc, String query) throws XPathExpressionException {
XPathExpression expr = xpathExpressions.get(query);
return (Node) expr.evaluate(doc, XPathConstants.NODE);
}
public static T selectValue(Class type, Node doc, String query) throws XPathExpressionException {
XPathExpression expr = xpathExpressions.get(query);
if (Number.class.equals(type)) {
Double v = (Double) expr.evaluate(doc, XPathConstants.NUMBER);
if (v.isNaN())
throw new XPathExpressionException("Query " + query + " result is not a number");
return type.cast(v);
} else if (String.class.equals(type)) {
return type.cast(expr.evaluate(doc, XPathConstants.STRING));
} else if (Boolean.class.equals(type)) {
return type.cast(expr.evaluate(doc, XPathConstants.BOOLEAN));
} else {
throw new XPathExpressionException("Unkown data type " + type);
}
}
public static Document readDocumentFromStream(InputStream stream) throws ParserConfigurationException, SAXException,
IOException {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse(stream);
return doc;
}
}
Basically this XMLUtil class lets you write simplified XPath queries like the ones below.
String s = XmlUtil.selectValue(String.class, "@Name");
float x = XmlUtil.selectValue(Number.class, node, "Vector2/X/text()").floatValue();[/cc]
Now that the XML/XPath part is somewhat taken care of here is the class that does most of the work.
public class Gleed2DItem {
private final Node node;
public Gleed2DItem(Node node) throws XPathException {
this.node = node.cloneNode(true);
}
private T property(Class type, String name) {
try {
return XmlUtil.selectValue(type, node, name);
} catch (XPathExpressionException e) {
throw new RuntimeException(e);
}
}
public String name() {
return property(String.class, "@Name");
}
public String type() {
return node.getAttributes().getNamedItem("xsi:type").getTextContent();
}
public boolean visible() {
return property(Boolean.class, "@Visible");
}
public Vec2 position() {
float x = property(Number.class, "Position/X/text()").floatValue();
float y = property(Number.class, "Position/Y/text()").floatValue();
return new Vec2(x, y);
}
public Vec2 origin() {
float x = property(Number.class, "Origin/X/text()").floatValue();
float y = property(Number.class, "Origin/Y/text()").floatValue();
return new Vec2(x, y);
}
public float rotation() {
return property(Number.class, "Rotation/text()").floatValue();
}
public float height() {
return property(Number.class, "Height/text()").floatValue();
}
public float width() {
return property(Number.class, "Width/text()").floatValue();
}
public float radius() {
return property(Number.class, "Radius/text()").floatValue();
}
public int fillColor() {
int r = property(Number.class, "FillColor/R/text()").intValue();
int g = property(Number.class, "FillColor/G/text()").intValue();
int b = property(Number.class, "FillColor/B/text()").intValue();
int a = property(Number.class, "FillColor/A/text()").intValue();
return Colors.rgba(r, g, b, a);
}
public int tintColor() {
int r = property(Number.class, "TintColor/R/text()").intValue();
int g = property(Number.class, "TintColor/G/text()").intValue();
int b = property(Number.class, "TintColor/B/text()").intValue();
int a = property(Number.class, "TintColor/A/text()").intValue();
return Colors.rgba(r, g, b, a);
}
public String textureFilename() {
return property(String.class, "texture_filename/text()");
}
public List customProperties() {
try {
List result = new ArrayList();
NodeList customPropertyNodes = XmlUtil.selectNodeList(node, "CustomProperties/Property/@Name");
for (int i = 0; i Node localPointNode = customPropertyNodes.item(i);
result.add(localPointNode.getNodeValue());
}
return result;
} catch (XPathExpressionException e) {
throw new RuntimeException(e);
}
}
public String customProperty(String name, String notFoundValue) {
return customProperty(String.class, name, notFoundValue);
}
public String customProperty(String name) {
return customProperty(name, null);
}
public T customProperty(Class type, String name) {
return customProperty(type, name, null);
}
public T customProperty(Class type, String name, T notFoundValue) {
String query;
if (Boolean.class.equals(type)) {
query = "CustomProperties/Property[@Name='" + name + "']/boolean/text()";
} else {
query = "CustomProperties/Property[@Name='" + name + "']/string/text()";
}
try {
//empty string means not found
if ("".equals(XmlUtil.selectValue(String.class, node, query)))
return notFoundValue;
T value = null;
if (Number.class.equals(type) || String.class.equals(type) || Boolean.class.equals(type)) {
value = XmlUtil.selectValue(type, node, query);
} else if (Vec2.class.equals(type)) {
float x = XmlUtil.selectValue(Number.class, node,
"CustomProperties/Property[@Name='" + name + "']/Vector2/X/text()").floatValue();
float y = XmlUtil.selectValue(Number.class, node,
"CustomProperties/Property[@Name='" + name + "']/Vector2/Y/text()").floatValue();
value = type.cast(new Vec2(x, y));
} else {
throw new RuntimeException("Unkown data type " + type);
}
return value;
} catch (XPathException e) {
if (notFoundValue != null)
return notFoundValue;
throw new RuntimeException(e);
}
}
}
The Gleed2DItem wraps a GLEED2D XML node and gives you easy access to the relevant bits of it. I will not explain XML and XPath here as there is plenty of documentation and tutorials out there, just ask your favorite search engine for it. It now gets pretty easy to make sprites out of all these items:
Document doc = XmlUtil.readDocumentFromStream(Assets.getAsStream("Level1.xml"));
NodeList nodeList = XmlUtil.selectNodeList(doc, "/Level/Layers/Layer[@Name='Layer1']/Items/*");
for (int i = 0; i Gleed2DItem item = new Gleed2DItem(nodeList.item(i));
Vec2 position = item.position();
CoreImage image = CoreImage.load(item.textureFilename());
Sprite sprite = new ImageSprite(image, (position.x), (position.y));
sprite.setAnchor(0.5, 0.5);
add(sprite);
}
Download the source code.
