Non-tile based level editing with Box2D, PulpCore and GLEED2D

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
255
4294967295

false
false
Spirit.png
Spirit

32
32




455
280


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




325
225


0
1

255
255
255
255
4294967295

false
false
Man.png
Man

32
32




80
50


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




85
265


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




515
60


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




475
160


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




190
145


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




370
405


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




130
435


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




245
315


0
1

255
255
255
255
4294967295

false
false
Spirit.png
Spirit

32
32




520
420


0
1

255
255
255
255
4294967295

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>