6
\$\begingroup\$

I want to use Box2D physics and lighting with a .tmx map. How can I "convert" a tilemap to Box2D world? My basic idea is to go through the tiles on the map and create an object for them in Box2D.

I'm using the Tiled map editor for the tilemap, and the target platform is PC.

Firstly, is this a good solution for my idea? Or is this even possible?
Secondly, if yes, then I dont know how to get the tile's position.

My code so far:

public class WorldLoader {
public static TiledMap map;

public static void loadMap(String mapName) {
    map = new TmxMapLoader().load(mapName);
    TiledMapTileLayer collisionLayer = (TiledMapTileLayer) map.getLayers().get("collision");

    for (int x = 0; x < collisionLayer.getWidth(); x++) {
        for (int y = 0; y < collisionLayer.getHeight(); y++) {
            Cell cell = collisionLayer.getCell(x, y);

            if (cell != null && cell.getTile() != null) {
               // float tileX = collisionLayer.getProperties().containsKey("x");
               // float tileY = cell.getTile().getProperties().get("y", Integer.class);

                BodyDef bodyDef = new BodyDef();
                bodyDef.position.set(tileX, tileY);

                PolygonShape shape = new PolygonShape();
                shape.setAsBox(16, 16);

                Body body = GameWorld.world.createBody(bodyDef);
                body.createFixture(shape, 0f);

                shape.dispose();
            }
        }
    }

}

}

\$\endgroup\$
15
\$\begingroup\$

After banging my head against this problem for a day I found a wonderful tutorial on the very subject over at Sion Dream. I knew there was a way to use object layers!

In a nutshell, create an objects layer on your map (Tiled, Tide and the tutorial author's pick, Gleed, all provide this function), draw the shapes you want your Box2d static bodies to be, then use his MapBodyManager class (or your variation thereof) to convert them to Box2d bodies. Here's my variation on the relevant methods. The most important change I made? polygon.getVertices() and polyline.getVertices() became getTransformedVertices(). Forget that, and all your static bodies will be built at the origin!

public class MapBodyBuilder {

    // The pixels per tile. If your tiles are 16x16, this is set to 16f
    private static float ppt = 0;

    public static Array<Body> buildShapes(Map map, float pixels, World world) {
        ppt = pixels;
        MapObjects objects = map.getLayers().get("Obstacles").getObjects();

        Array<Body> bodies = new Array<Body>();

        for(MapObject object : objects) {

            if (object instanceof TextureMapObject) {
                continue;
            }

            Shape shape;

            if (object instanceof RectangleMapObject) {
                shape = getRectangle((RectangleMapObject)object);
            }
            else if (object instanceof PolygonMapObject) {
                shape = getPolygon((PolygonMapObject)object);
            }
            else if (object instanceof PolylineMapObject) {
                shape = getPolyline((PolylineMapObject)object);
            }
            else if (object instanceof CircleMapObject) {
                shape = getCircle((CircleMapObject)object);
            }
            else {
                continue;
            }

            BodyDef bd = new BodyDef();
            bd.type = BodyType.StaticBody;
            Body body = world.createBody(bd);
            body.createFixture(shape, 1);

            bodies.add(body);

            shape.dispose();
        }
        return bodies;
    }

    private static PolygonShape getRectangle(RectangleMapObject rectangleObject) {
        Rectangle rectangle = rectangleObject.getRectangle();
        PolygonShape polygon = new PolygonShape();
        Vector2 size = new Vector2((rectangle.x + rectangle.width * 0.5f) / ppt,
                                   (rectangle.y + rectangle.height * 0.5f ) / ppt);
        polygon.setAsBox(rectangle.width * 0.5f / ppt,
                         rectangle.height * 0.5f / ppt,
                         size,
                         0.0f);
        return polygon;
    }

    private static CircleShape getCircle(CircleMapObject circleObject) {
        Circle circle = circleObject.getCircle();
        CircleShape circleShape = new CircleShape();
        circleShape.setRadius(circle.radius / ppt);
        circleShape.setPosition(new Vector2(circle.x / ppt, circle.y / ppt));
        return circleShape;
    }

    private static PolygonShape getPolygon(PolygonMapObject polygonObject) {
        PolygonShape polygon = new PolygonShape();
        float[] vertices = polygonObject.getPolygon().getTransformedVertices();

        float[] worldVertices = new float[vertices.length];

        for (int i = 0; i < vertices.length; ++i) {
            System.out.println(vertices[i]);
            worldVertices[i] = vertices[i] / ppt;
        }

        polygon.set(worldVertices);
        return polygon;
    }

    private static ChainShape getPolyline(PolylineMapObject polylineObject) {
        float[] vertices = polylineObject.getPolyline().getTransformedVertices();
        Vector2[] worldVertices = new Vector2[vertices.length / 2];

        for (int i = 0; i < vertices.length / 2; ++i) {
            worldVertices[i] = new Vector2();
            worldVertices[i].x = vertices[i * 2] / ppt;
            worldVertices[i].y = vertices[i * 2 + 1] / ppt;
        }

        ChainShape chain = new ChainShape(); 
        chain.createChain(worldVertices);
        return chain;
    }
}

The best part of this method is that your shapes can be polygons or even circles, allowing for more than the basic square tiles and squared-off platforms (think hills!). Not only that, but you can create fewer, larger bodies without having to programmatically work out whether two adjacent tiles are visually part of the same object.

(Edit: the code wasn't quite working properly before. The change mentioned above (to getTransformedVertices()) fixed it. Also removed references to my utility classes, since they weren't relevant to the discussion.)

\$\endgroup\$
  • 1
    \$\begingroup\$ Hi, I'm the guy who wrote the tutorial. Thanks for referencing it! Also thanks for the corrections, they are of great help, will update the article. \$\endgroup\$ – David Saltares Mar 10 '14 at 20:44
  • \$\begingroup\$ Hey, thanks for the tutorial! I really do think it was the only proper solution to this on the whole web! \$\endgroup\$ – daemonexmachina Mar 10 '14 at 20:51
  • \$\begingroup\$ Good to hear that! I added your changes to the tutorial and updated the link. \$\endgroup\$ – David Saltares Mar 10 '14 at 21:59
  • \$\begingroup\$ Any way to make this work with isometric tiled maps? \$\endgroup\$ – Vincent Williams Jul 1 '15 at 3:45
  • \$\begingroup\$ I know I'm pretty late but... this is amazing! Thanks for the code. \$\endgroup\$ – Nijraj Gelani Dec 25 '15 at 10:05
3
\$\begingroup\$

Yes, you want to iterate over the tilemap in x and y (inner and outer loops).

How large are your maps (n * m)? If quite small / sparse, you could use individual bodies for every solid tile. But this would rapidly get out of hand even though these would be static bodies, so you'd need to really keep your levels tight / tilemaps small. I probably would try to stay under a couple to a few hundred bodies for mobile, so you may have to generate maps accordingly in this case, i.e. use fewer obstacles.

If larger, then you need to conjoin adjacent obstacle tiles -- mainly walls and other multi-tile objects: If you're using an external app to generate your tilemap, you will need to examine the map and evaluate large contiguous sections of wall, store these in a separate data structure, and then convert each of these into large b2Bodys. In this process, it can be useful to eliminate colinear vertices before creating your b2Bodys from them, because the more vertices a body has, the more CPU time it takes to process.

If you're creating your tilemap at runtime instead, then I would suggest that you start with long objects, and use these as a kind of multi-tile "stamp" to create the tile map, while at the same time storing the "stamp" positions in another data structure (a list?). You could then use the secondary data structure for the creation of large b2Bodys. This keeps you from having to do the tedious task described in the previous paragraph.

The point here is to reduce the number of bodies so your physics runs smoothly.

As for your tile's position: This is implicit in the 2D array indices. Let's say you consider every successive index (in x and y) in your tilemap to be a 2m distance. That would mean that the tile at index [0, 3] in your array is at position {0m, 6m} from the origin. So as you iterate on each index x and y, use these indices to determine the world position of the b2Body to be created from that 2D index.

\$\endgroup\$
0
\$\begingroup\$

Answer by daemonaka pretty much covers the issue, I've expanded it for ellipses (not enough points to write comment so have to write an answer). Maybe somebody will find it useful:

Firstly in buildShapes method you add case for ellipses:

...
else if (object instanceof EllipseMapObject) {
    shape = getEllipse((EllipseMapObject) object);
}
...

And getEllipse looks as follows:

private static final int ELLIPSE_APPROX_POINTS = 32;
private static Vector2[] ellipseVertices = new Vector2[ELLIPSE_APPROX_POINTS];

private static ChainShape getEllipse(EllipseMapObject ellipseMapObject) {
    for (int idx = 0; idx < ELLIPSE_APPROX_POINTS; idx++) {
        float percentDone = (float) idx / (float) ELLIPSE_APPROX_POINTS;
        float currentEllipseAngle = percentDone * 2 * MathUtils.PI;
        Vector2 newPoint = calculatePointOnEllipseForAngle(currentEllipseAngle, ellipseMapObject.getEllipse());
        ellipseVertices[idx] = newPoint;
    }

    ChainShape chainShape = new ChainShape();
    chainShape.createChain(ellipseVertices);
    return chainShape;
}

private static Vector2 calculatePointOnEllipseForAngle(float currentEllipseAngle, Ellipse ellipse) {
    Vector2 newPoint = new Vector2();
    float radiusX = ellipse.width / 2f;
    float radiusY = ellipse.height / 2f;

    newPoint.x = (radiusX * (float) Math.cos(currentEllipseAngle) + (ellipse.x + radiusX)) / ppt;
    newPoint.y = (radiusY * (float) Math.sin(currentEllipseAngle) + (ellipse.y + radiusY)) / ppt;

    return newPoint;
}

Code is based on https://stackoverflow.com/questions/22694850/approximating-an-ellipse-with-a-polygon. Hope this helps someone.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.