Libgdx, Box2D and a huge "block world" (destructible blocks!)

Hello everyone!

Right now I’m still in the planning phase of a my very first game. I’m creating a “Minecraft”-like game in 2D that features blocks that can be destroyed as well as players moving around the map.

For creating the map I chose a 2D-Array of Integers that represent the Block ID. For testing purposes I created a huge map (16348 * 256) and in my prototype that didn’t use Box2D everything worked like a charm.

I only rendered those blocks that where within the bounds of my camera and got 60 fps straight. The problem stared when I decided to use an existing physics-solution rather than implementing my own one. What I had was basically simple hitboxes around the blocks and then I had to manually check if the player collided with any of those in his neighborhood.

For more advanced physics as well as the collision detection I want to switch over to Box2D.
The problem I have right now is … how to go about the bodies?

I mean, the blocks are of a static bodytype. They don’t move on their own, they just are there to be collided with. But as far as I can see it, every block needs his own body with a rectangular fixture attached to it, so as to be destroyable.

But for a huge map such as mine, this turns out to be a real performance bottle-neck. (In fact even a rather small map [compared to the other] of 1024*256 is unplayable.)
I mean I create thousands of thousands of blocks. Even if I just render those that are in my immediate neighbourhood there are hundreds of them and (at least with the debugRenderer) I drop to 1 fps really quickly (on my own “monster machine”).

I thought about strategies like creating just one body, attaching multiple fixtures and only if a fixture got hit, separate it from the body, create a new one and destroy it, but this didn’t turn out quite as successful as hoped. (In fact the core just dumps. Ah hello C! I really missed you :X)

Here is the code:


public class Box2DGameScreen implements Screen
{
    private World world;
    private Box2DDebugRenderer debugRenderer;
    private OrthographicCamera camera;

    private final float TIMESTEP = 1 / 60f; // 1/60 of a second -> 1 frame per second
    private final int VELOCITYITERATIONS = 8;
    private final int POSITIONITERATIONS = 3;

    private Map map;
    private BodyDef blockBodyDef;
    private FixtureDef blockFixtureDef;

    private BodyDef groundDef;
    private Body ground;

    private PolygonShape rectangleShape;

    @Override
    public void show()
    {
        world = new World(new Vector2(0, -9.81f), true);
        debugRenderer = new Box2DDebugRenderer();
        camera = new OrthographicCamera();
        // Pixel:Meter = 16:1

        // Body definition
        BodyDef ballDef = new BodyDef();
        ballDef.type = BodyDef.BodyType.DynamicBody;
        ballDef.position.set(0, 1);

        // Fixture definition
        FixtureDef ballFixtureDef = new FixtureDef();
        ballFixtureDef.shape = new CircleShape();
        ballFixtureDef.shape.setRadius(.5f); // 0,5 meter
        ballFixtureDef.restitution = 0.75f; // between 0 (not jumping up at all) and 1 (jumping up the same amount as it fell down)
        ballFixtureDef.density = 2.5f; // kg / m²
        ballFixtureDef.friction = 0.25f; // between 0 (sliding like ice) and 1 (not sliding)

    //    world.createBody(ballDef).createFixture(ballFixtureDef);

        groundDef = new BodyDef();
        groundDef.type = BodyDef.BodyType.StaticBody;
        groundDef.position.set(0, 0);

        ground = world.createBody(groundDef);

        this.map = new Map(20, 20);

        rectangleShape = new PolygonShape();
    //    rectangleShape.setAsBox(1, 1);

        blockFixtureDef = new FixtureDef();
    //    blockFixtureDef.shape = rectangleShape;
        blockFixtureDef.restitution = 0.1f;
        blockFixtureDef.density = 10f;
        blockFixtureDef.friction = 0.9f;
    }

    @Override
    public void render(float delta)
    {
        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);


        debugRenderer.render(world, camera.combined);
        drawMap();

        world.step(TIMESTEP, VELOCITYITERATIONS, POSITIONITERATIONS);
    }

    private void drawMap()
    {

        for(int a = 0; a < map.getHeight(); a++)
        {
        /*
            if(camera.position.y - (camera.viewportHeight/2) > a)
                continue;

            if(camera.position.y - (camera.viewportHeight/2) < a)
                break;
        */
            for(int b = 0; b < map.getWidth(); b++)
            {
            /*    if(camera.position.x - (camera.viewportWidth/2) > b)
                    continue;

                if(camera.position.x - (camera.viewportWidth/2) < b)
                    break;
            */
            /*
                blockBodyDef = new BodyDef();
                blockBodyDef.type = BodyDef.BodyType.StaticBody;
                blockBodyDef.position.set(b, a);

                world.createBody(blockBodyDef).createFixture(blockFixtureDef);
            */
                PolygonShape rectangleShape = new PolygonShape();
                rectangleShape.setAsBox(b, a, new Vector2(b, a), 0);

                blockFixtureDef.shape = rectangleShape;

                ground.createFixture(blockFixtureDef);
                rectangleShape.dispose();

            }
        }
    }

    @Override
    public void resize(int width, int height)
    {
        camera.viewportWidth = width / 16;
        camera.viewportHeight = height / 16;
        camera.update();
    }

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void pause() {

    }

    @Override
    public void resume() {

    }

    @Override
    public void dispose() {
        world.dispose();
        debugRenderer.dispose();
    }
}


As you can see I’m facing multiple problems here. I’m not quite sure how to check for the bounds but also if the map is bigger than 2424 like 1024256 Java just crashes -.-.
And with 24*24 I get like 9 fps. So I’m doing something really terrible here, it seems and I assume that there most be a (much more performant) way, even with Box2D’s awesome physics.

Any other ideas?

Thanks in advance!

Well, actually I’ve managed to get this far:


 private static final int WIDTH = 8;
    private static final int HEIGHT = 6;

    private World world;
    private Box2DDebugRenderer debugRenderer;
    private OrthographicCamera camera;

    private final float TIMESTEP = 1 / 60f; // 1/60 of a second -> 1 frame per second
    private final int VELOCITYITERATIONS = 8;
    private final int POSITIONITERATIONS = 3;

    private Map map;
    private BodyDef blockBodyDef;
    private FixtureDef blockFixtureDef;

    private BodyDef groundDef;
    private Body ground;

    private PolygonShape rectangleShape;

    @Override
    public void show()
    {
        world = new World(new Vector2(0, -9.81f), true);
        debugRenderer = new Box2DDebugRenderer();

        camera = new OrthographicCamera(WIDTH, HEIGHT);
        camera.setToOrtho(false, WIDTH, HEIGHT);
        // Pixel:Meter = 16:1

        // Body definition
        BodyDef ballDef = new BodyDef();
        ballDef.type = BodyDef.BodyType.DynamicBody;
        ballDef.position.set(0, 1);

        // Fixture definition
        FixtureDef ballFixtureDef = new FixtureDef();
        ballFixtureDef.shape = new CircleShape();
        ballFixtureDef.shape.setRadius(.5f); // 0,5 meter
        ballFixtureDef.restitution = 0.75f; // between 0 (not jumping up at all) and 1 (jumping up the same amount as it fell down)
        ballFixtureDef.density = 2.5f; // kg / m²
        ballFixtureDef.friction = 0.25f; // between 0 (sliding like ice) and 1 (not sliding)

    //    world.createBody(ballDef).createFixture(ballFixtureDef);

        groundDef = new BodyDef();
        groundDef.type = BodyDef.BodyType.StaticBody;
        groundDef.position.set(0, 0);

        ground = world.createBody(groundDef);

        this.map = new Map(1024, 256);

        rectangleShape = new PolygonShape();
    //    rectangleShape.setAsBox(1, 1);

        blockFixtureDef = new FixtureDef();
    //    blockFixtureDef.shape = rectangleShape;
        blockFixtureDef.restitution = 0.1f;
        blockFixtureDef.density = 10f;
        blockFixtureDef.friction = 0.9f;
    }

    @Override
    public void render(float delta)
    {
    //    System.out.println("c.x: " + camera.position.x + " c.y: " + camera.position.y);
    //    System.out.println(camera.viewportWidth + " " + camera.viewportHeight);

        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        debugRenderer.render(world, camera.combined);
        drawMap();

        world.step(TIMESTEP, VELOCITYITERATIONS, POSITIONITERATIONS);
    }

    private void drawMap()
    {
        for(int a = 0; a < map.getHeight(); a++)
        {
            // Bounds check (y)
            if((camera.position.y - (camera.viewportHeight / 2)) > a)
                continue;

            if((camera.position.y + (camera.viewportHeight / 2)) < a)
                break;

            for(int b = 0; b < map.getWidth(); b++)
            {
                // Bounds check (x)
                // Block to the left? If so, continue (no need to render, but we need to check the others)
                if((camera.position.x - (camera.viewportWidth / 2)) > b)
                    continue;

                // Blocks to the right? If so, break (no need to check even bigger ones)
                if((camera.position.x + (camera.viewportWidth / 2)) < b)
                    break;

            /*
                blockBodyDef = new BodyDef();
                blockBodyDef.type = BodyDef.BodyType.StaticBody;
                blockBodyDef.position.set(b, a);

                world.createBody(blockBodyDef).createFixture(blockFixtureDef);
            */
                PolygonShape rectangleShape = new PolygonShape();
                rectangleShape.setAsBox(1, 1, new Vector2(b, a), 0);

                blockFixtureDef.shape = rectangleShape;

                ground.createFixture(blockFixtureDef);
                rectangleShape.dispose();

            }
        }
    }

    @Override
    public void resize(int width, int height)
    {
        Gdx.gl.glViewport(0, 0, width, height);
    }

Still FPS are dropping from 60 to 19 and below in no time, without any interaction whatsoever :frowning:

So you call the drawmap method in the render class, so every frame. Any reason why you are creating a fixture every time?

Don’t tell me you are create fixtures every frame o-o Also, only add blocks around certain area of the player. When the player moves, remove blocks that are too far from the player, and add new ones.

Also, you might consider using something like this. (Don’t know how this is called)

Basically you only add 6 fixtures / bodies(in this example) to Box2d instead of fixture / tile.

Well, actually I thought that it was more performant to draw and update (hence create) the fixtures in the neighbourhood of the player every frame, instead of putting like 16384*256 of them in the memory at once. I’m not even sure how I would not render them, because the DebugRenderer (for now) does all the heavy lifting for me and even if I switch my SpriteBatch later on, I fear that I could only prevent the sprites that are too far away from being rendered but the physics would be calculated for the whole map :frowning:

Not sure how to do that with Box2D, without creating/deleting them every frame.

Ya, I heard about that, but how do I know when and where to split if a single block-tile gets destroyed?

Also, if I create them only once (in the show()-method) and only those within the bounds of my camera, I only get around ~30fps. I want my tiles to be about 16 pixels in size. Sure, if I increase their size and make them really big so that only 5 of them render it’s not a problem at all.

Here is how you only update when player moves to another chunk


// somewhere in the other scope, for example inside the player class
		int lastChunkX, lastChunkY;
		

// inside player.update
		int chunkX=player.x/chunkSize;
		int chunkY=player.y/chunkSize;
		
		if(chunkX != lastChunkX || chunkY != lastChunkY) {
			// player has moved to another chunk, update box2d
			removeBodies();
			addBodiesFor(chunkX, chunkY);
		}
		
		lastChunkX = chunkX;
		lastChunkY = lastChunkY;

If player destroys the block, you can probably just remove all the bodies from box2d and calculate again.

So, hm, I don’t create bodies right now, just stuck with the fixtures.

Still, I have no idea why this works.

I call createBlocks() exactly ONCE in the show-method and I never update the blocks.

Yet I can move around the map and it renders correctly oO

I assume there is some black magic involved in the Box2DDebugRenderer :X



    @Override
    public void show()
    {
        world = new World(new Vector2(0, -9.81f), true);
        debugRenderer = new Box2DDebugRenderer();

        camera = new OrthographicCamera(WIDTH, HEIGHT);
        camera.setToOrtho(false, WIDTH, HEIGHT);

        // Body definition
        BodyDef ballDef = new BodyDef();
        ballDef.type = BodyDef.BodyType.DynamicBody;
        ballDef.position.set(camera.viewportWidth/2, camera.viewportHeight/2);

        // Fixture definition
        FixtureDef ballFixtureDef = new FixtureDef();
        ballFixtureDef.shape = new CircleShape();
        ballFixtureDef.shape.setRadius(2.5f); // 0,5 meter
        ballFixtureDef.restitution = 0.75f; // between 0 (not jumping up at all) and 1 (jumping up the same amount as it fell down)
        ballFixtureDef.density = 2.5f; // kg / m²
        ballFixtureDef.friction = 0.25f; // between 0 (sliding like ice) and 1 (not sliding)

    //    world.createBody(ballDef).createFixture(ballFixtureDef);

        groundDef = new BodyDef();
        groundDef.type = BodyDef.BodyType.StaticBody;
        groundDef.position.set(0, 0);

        ground = world.createBody(groundDef);

        this.map = new Map(16384, 256);

        rectangleShape = new PolygonShape();
    //    rectangleShape.setAsBox(1, 1);

        blockFixtureDef = new FixtureDef();
    //    blockFixtureDef.shape = rectangleShape;
        blockFixtureDef.restitution = 0.1f;
        blockFixtureDef.density = 10f;
        blockFixtureDef.friction = 0.9f;

        createBlocks();
    }

    @Override
    public void render(float delta)
    {
        camera.update();

        if(Gdx.input.isKeyPressed(Input.Keys.LEFT))
        {
                camera.translate(-3, 0, 0);
        }

        if(Gdx.input.isKeyPressed(Input.Keys.RIGHT))
        {
                camera.translate(3, 0, 0);
        }

        if(Gdx.input.isKeyPressed(Input.Keys.DOWN))
        {
                camera.translate(0, -3, 0);
        }

        if(Gdx.input.isKeyPressed(Input.Keys.UP))
        {
                camera.translate(0, 3, 0);
        }


    //    System.out.println("c.x: " + camera.position.x + " c.y: " + camera.position.y);
    //    System.out.println(camera.viewportWidth + " " + camera.viewportHeight);

        Gdx.gl.glClearColor(1, 1, 1, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        debugRenderer.render(world, camera.combined);
    //    drawMap();

        world.step(TIMESTEP, VELOCITYITERATIONS, POSITIONITERATIONS);
    }

    private void createBlocks()
    {
        for(int a = 0; a < (camera.position.y + (camera.viewportHeight / 2)); a++)
        {
            // Bounds check (y)
            if((camera.position.y - (camera.viewportHeight / 2)) > a)
                continue;

            for(int b = 0; b < (camera.position.x + (camera.viewportWidth / 2)); b++)
            {
                // Bounds check (x)
                // Block to the left? If so, continue (no need to render, but we need to check the others)
                if((camera.position.x - (camera.viewportWidth / 2)) > b * PIXELTOMETER)
                    continue;

                if(map.getTileMap()[a][b] != 1) {
                    PolygonShape rectangleShape = new PolygonShape();
                    rectangleShape.setAsBox(0.5f * PIXELTOMETER, 0.5f * PIXELTOMETER, new Vector2(b * PIXELTOMETER, a * PIXELTOMETER), 0);
                    blockFixtureDef.shape = rectangleShape;

                    ground.createFixture(blockFixtureDef);
                    rectangleShape.dispose();
                }
            }
        }
    }


The Map is HUGE, as you can see and I still get 40-50 fps while moving around and apart from it being a bit laggy it works great. (I’ll try to fix this with a bigger radius around the camera, so it doesn’t have to update those at the edge, so I get a bit of a “buffer”)

Thanks a lot! Currently I’m thinking about sticking with the fixture idea. So the whole ground is ONE body and I add fixtures for every block and only remove those when one block is destroyed. (So a block would be a fixture, not a body. I’m not sure if this is more performant but I think so, actually, because instead of 2n (one body for every fixture) objects I only need n+1 (The amount of fixtures and one body)

I have a little bit of progress to talk about: Now I divide my map in different chunks of the same size.

Currently without Box2D.
I’m not sure Box2D will be a real option here, sadly. I’d love to use it for Collision-detection and to spare me reinventing the wheel but a chunk consists of 64*64 blocks currently.

Each block is 16 pixels in size. So I’d have to calculate 4096 bodies and the same amount of fixtures or 4097 objects (one body, 4096 fixtures) just for this. Box2D performs very bad with these amounts, at least for me.

So I’m not sure I can really use it :frowning: