Category Archives: Box2D

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.

Mapping Box2D shapes from textures

Pretty soon after getting started with Box2D and having a ball bouncing on the screen the novelty wears off, playing with circles and boxes is not much fun. I wanted more complex shapes and it turns out that creating polygons by hand is boring and error prone and so is approximating a shape from primitives. I needed something that would trace an image and create a  polygon out of it, at run-time if possible. This lead to searching the internets high and low but I could not find anything other that other people asking for the same thing I was looking for.

Eventually I found a piece of code from the Farseer project (a 2d physics engine written in C#) that did exactly what I needed except, well, it was written in C#. The code was contributed by a user going by the alias Sickbattery, he is not listed as a project developer and there is nothing else about him but he deserves the credit for the original C# code.

An hour later I finished translating the C# source to Java, the code is not pretty and exhibits a bit of foreign language accent but it works  pretty well. I will not list the code here, scroll to the end of the article to download it.

There are few static functions that can be called, all of them returning a list of Vec2 vertices almost ready to be used to create Box2D polygons. Another hour and a hard to find post on the Box2D forums later and I finally got the thing working, here are the relevant bits of code.

ImageSprite shape = new ImageSprite("Shape.png", 0, 0);
//get the list of vertices approximating the image
List verts = TextureConverter.createPolygon(shape.getImage().getData(), shape.width.getAsInt(),
shape.height.getAsInt());
//scale the vertices to the world's scale
for (Vec2 v : verts) {
v.mulLocal(1 / scale);
}
//create a polygon from all these vertices
Polygon poly = new Polygon(verts.toArray(new Vec2[] {}));
Polygon tracedPoly = Polygon.traceEdge(poly);
BodyDef bd = new BodyDef();
bd.position.set(x, y);
Body body = world.createBody(bd);
//create a prototype polygon definition that will be used for the characteristics of all shapes
PolygonDef proto = new PolygonDef();
proto.friction = 0.2f;
proto.restitution = 0.8f;
proto.density = 2f;
//decompose the polygon in convex pieces and add them to the body
Polygon.decomposeConvexAndAddTo(tracedPoly, body, proto);
body.setMassFromShapes();

The sample uses the JBox2dD debug drawing classes from a previous post, so you can see the shapes that were generated. Press ‘D’ to toggle debug drawing, press ‘R’ to restart, click to add another shape.

Download the source code, PulpCore and JBox2D not included.

Debugging JBox2d with PulpCore

JBox2D is pretty easy to use until it isn’t, so when that happens it is quite helpful to get some details on what is going on in the physics world. The Box2D manual is somewhat cryptic and so is the JBox2D port, there is a DebugDraw class as promised in the docs but not much of a hint on how to use it. Luckily there is source code and it “explains” things well enough.

Scroll all the way to the end of this post to find out how things turned out or keep reading to see how I got there.

First step is indeed extending the org.jbox2d.dynamics.DebugDraw class and implementing its drawing methods, note that I will using the java.awt.Graphics2D instead of PulpCore’s CoreGraphics class, mostly because I did not wanted to implement circle drawing (Yes, I know it is simple enough, but why bother?)

public class PulpCoreDebugDraw extends DebugDraw {
private Graphics2D g;
private final float scale;

public PulpCoreDebugDraw(int width, int height, float scale) {
super(new OBBViewportTransform());
this.scale = scale;
viewportTransform.setCamera(width / 2 / scale, height / 2 / scale, scale);
viewportTransform.setExtents(width / 2, height / 2);
}

public void setGraphics(Graphics2D g) {
this.g = g;
}

@Override
public void drawCircle(Vec2 center, float radius, Color3f color) {
Color oldColor = g.getColor();
g.setColor(new Color(color.x / 255, color.y / 255, color.z / 255));
Vec2 v = new Vec2(center.x - radius, center.y - radius);
viewportTransform.getWorldToScreen(v, v);
int r = (int) (radius * scale);
g.drawArc((int) v.x, (int) v.y, 2 * r, 2 * r, 0, 360);
g.setColor(oldColor);
}

Now wrap the PulpCoreDebugDraw into a Sprite so that it can be added to Scene2D. The Java2DSprite class can be found in the contrib directory of the PulpCore distribution.

public class JBox2DDebugSprite extends Java2DSprite {
private final World world;
private PulpCoreDebugDraw debugDraw;

public JBox2DDebugSprite(World world, int x, int y, int w, int h, float scale) {
super(x, y, w, h);
this.world = world;
debugDraw = new PulpCoreDebugDraw(w, h, scale);
world.setDebugDraw(debugDraw);
}

@Override
protected void draw(Graphics2D g) {
debugDraw.setGraphics(g);
world.drawDebugData();
}

@Override
protected boolean needsRedraw() {
return true;
}
}

All the is left is adding the JBox2DDebugSprite to your scene and telling the JBox2D World to use it. The debug flags are explained in the DebugDraw source code.

world.setDrawDebugData(true);
add(new JBox2DDebugSprite(world, 0, 0, Stage.getWidth(), Stage.getHeight(), WORLD_TO_STAGE_SCALE));
world.getDebugDraw().setFlags(
DebugDraw.e_shapeBit | DebugDraw.e_coreShapeBit | DebugDraw.e_jointBit | DebugDraw.e_controllerBit);

Download the source code, PulpCore and JBox2D not included.