Java-Gaming.org Hi !
Featured games (83)
games approved by the League of Dukes
Games in Showcase (542)
Games in Android Showcase (133)
games submitted by our members
Games in WIP (606)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1]
  ignore  |  Print  
  Libgdx, Box2D and a huge "block world" (destructible blocks!)  (Read 902 times)
0 Members and 1 Guest are viewing this topic.
Offline BadBreath

Senior Newbie





« Posted 2014-08-18 20:43:30 »

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:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
109  
110  
111  
112  
113  
114  
115  
116  
117  
118  
119  
120  
121  
122  
123  
124  
125  
126  
127  
128  
129  
130  
131  
132  
133  
134  
135  
136  
137  
138  
139  
140  
141  
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 24*24 like 1024*256 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!
Offline BadBreath

Senior Newbie





« Reply #1 - Posted 2014-08-18 22:44:18 »

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

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
109  
110  
111  
112  
113  
114  
115  
116  
117  
118  
119  
120  
121  
122  
123  
124  
 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 Sad
Offline Dragon

Senior Newbie


Medals: 1
Exp: 2 years



« Reply #2 - Posted 2014-08-19 02:16:49 »

So you call the drawmap method in the render class, so every frame. Any reason why you are creating a fixture every time?
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline trollwarrior1
« Reply #3 - Posted 2014-08-19 06:52:40 »

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.
Offline BadBreath

Senior Newbie





« Reply #4 - Posted 2014-08-19 08:35:35 »

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

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 Sad

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.

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

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.

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.
Offline trollwarrior1
« Reply #5 - Posted 2014-08-19 08:42:40 »

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

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
// 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.
Offline BadBreath

Senior Newbie





« Reply #6 - Posted 2014-08-19 08:50:20 »

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

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
85  
86  
87  
88  
89  
90  
91  
92  
93  
94  
95  
96  
97  
98  
99  
100  
101  
102  
103  
104  
105  
106  
107  
108  
    @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")
Offline BadBreath

Senior Newbie





« Reply #7 - Posted 2014-08-19 08:53:02 »

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

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
// 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.

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)
Offline BadBreath

Senior Newbie





« Reply #8 - Posted 2014-08-20 11:35:28 »

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 Sad
Pages: [1]
  ignore  |  Print  
 
 

 

Add your game by posting it in the WIP section,
or publish it in Showcase.

The first screenshot will be displayed as a thumbnail.

Elsealabs (17 views)
2014-12-28 10:39:27

CopyableCougar4 (20 views)
2014-12-28 02:10:29

BurntPizza (25 views)
2014-12-27 22:38:51

Mr.CodeIt (15 views)
2014-12-27 04:03:04

TheDudeFromCI (20 views)
2014-12-27 02:14:49

Mr.CodeIt (26 views)
2014-12-23 03:34:11

rwatson462 (58 views)
2014-12-15 09:26:44

Mr.CodeIt (47 views)
2014-12-14 19:50:38

BurntPizza (98 views)
2014-12-09 22:41:13

BurntPizza (116 views)
2014-12-08 04:46:31
How do I start Java Game Development?
by gouessej
2014-12-27 19:41:21

Resources for WIP games
by kpars
2014-12-18 10:26:14

Understanding relations between setOrigin, setScale and setPosition in libGdx
by mbabuskov
2014-10-09 22:35:00

Definite guide to supporting multiple device resolutions on Android (2014)
by mbabuskov
2014-10-02 22:36:02

List of Learning Resources
by Longor1996
2014-08-16 10:40:00

List of Learning Resources
by SilverTiger
2014-08-05 19:33:27

Resources for WIP games
by CogWheelz
2014-08-01 16:20:17

Resources for WIP games
by CogWheelz
2014-08-01 16:19:50
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!