6
\$\begingroup\$

A few days ago I decided that I wanted to get involved with libgdx. So far I'm blown away by how simple it is to get something on the screen to work with.

I'm trying to make a very simple tech demo where stars fall from the sky that can get collected by a player controlled cloud. Stars that get not collected will be left on the bottom on the screen.

The purpose of this tech demo is to test the performance on various devices so that I can get a feel of what I can do with libgdx (Box2D) and what I can not do.

So far this is working fine but I quickly run into performance issues on my galaxyS5. After I have like 500 stars at the screen the FPS starts to drop.

I would kindly ask you libgdx experts to point out critical flaws in my code that are bad for performance.

The project consists of the following four classes:

Game.java

package game;

import java.util.ArrayList;
import java.util.Random;

import game.entities.Collectable1;
import game.entities.EntityConstants;
import game.utilities.Utility;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.EdgeShape;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Pool;

public class MyGame extends ApplicationAdapter implements InputProcessor {
    SpriteBatch batch;
    Sprite spritePlayer;
    Texture img;
    World world;
    Body body;
    Body bodyEdgeScreen;
    Matrix4 debugMatrix;
    OrthographicCamera camera;
    BitmapFont font;

    float deltaTimeSum;

    float torque = 0.0f;
    boolean drawSprite = true;

    Random random = new Random();

    FPSLogger fpsLogger = new FPSLogger();

    private Texture collectable1Image;

    public static final int GAME_WIDTH = 1920;
    public static final int GAME_HEIGHT = 1080;

    // array containing the active Collectable1s.
    private final ArrayList<Collectable1> activeCollectable1s = new ArrayList<Collectable1>();

    // Collectable1s pool.
    private final Pool<Collectable1> collectable1Pool = new Pool<Collectable1>() {
        @Override
        protected Collectable1 newObject() {
            return new Collectable1();
        }
    };
    private long score = 0;

    @Override
    public void create() {

        batch = new SpriteBatch();
        img = new Texture("cloud.png");
        collectable1Image = new Texture("Collectable1.png");
        spritePlayer = new Sprite(img);

        spritePlayer.setPosition(-spritePlayer.getWidth() / 2, -spritePlayer.getHeight() / 2);

        world = new World(new Vector2(0, -1f), true);
        createWorldContactListener();

        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyDef.BodyType.DynamicBody;
        bodyDef.position.set(Utility.convertToMeter(spritePlayer.getX() + spritePlayer.getWidth() / 2), Utility.convertToMeter(spritePlayer.getY() + spritePlayer.getHeight() / 2));

        body = world.createBody(bodyDef);

        PolygonShape shape = new PolygonShape();
        shape.setAsBox(Utility.convertToMeter(spritePlayer.getWidth() / 2), Utility.convertToMeter(spritePlayer.getHeight() / 2));

        FixtureDef fixtureDef = new FixtureDef();
        fixtureDef.shape = shape;
        fixtureDef.density = 0.1f;
        fixtureDef.restitution = 0.5f;
        fixtureDef.filter.categoryBits = EntityConstants.PHYSICS_ENTITY;
        fixtureDef.filter.maskBits = EntityConstants.WORLD_ENTITY | EntityConstants.PHYSICS_ENTITY;

        body.createFixture(fixtureDef);

        shape.dispose();

        float w = Utility.convertToMeter(GAME_WIDTH);
        float h = Utility.convertToMeter(GAME_HEIGHT);
        float meter50 = Utility.convertToMeter(50);

        createStaticWall(-w / 2, (-h + meter50) / 2, w / 2, (-h + meter50) / 2);
        createStaticWall((-w + meter50) / 2, -h / 2, (-w + meter50) / 2, h / 2);
        createStaticWall((w - meter50) / 2, -h / 2, (w - meter50) / 2, h / 2);

        Gdx.input.setInputProcessor(this);

        // debugRenderer = new Box2DDebugRenderer();
        font = new BitmapFont();
        font.setColor(Color.YELLOW);

        camera = new OrthographicCamera(GAME_WIDTH, GAME_HEIGHT);

    }

    private void createWorldContactListener() {

        world.setContactListener(new ContactListener() {

            @Override
            public void beginContact(Contact contact) {
                if (contact.getFixtureA().getBody().equals(body) || contact.getFixtureB().getBody().equals(body)) {
                    System.out.println("Checking body collision");
                    for (int i = 0; i < activeCollectable1s.size(); i++) {
                        Collectable1 collectable1 = activeCollectable1s.get(i);

                        // Contact

                        if (contact.getFixtureA().getBody().equals(collectable1.getBody()) || (contact.getFixtureB().getBody().equals(collectable1.getBody()))) {
                            collectable1.alive = false;
                            activeCollectable1s.remove(collectable1);
                            collectable1Pool.free(collectable1);
                            score++;
                        }
                    }

                }

            }

            @Override
            public void endContact(Contact contact) {
                // TODO Auto-generated method stub

            }

            @Override
            public void preSolve(Contact contact, Manifold oldManifold) {
                // TODO Auto-generated method stub

            }

            @Override
            public void postSolve(Contact contact, ContactImpulse impulse) {
                // TODO Auto-generated method stub

            }

        });

    }

    private void createStaticWall(float x1, float y1, float x2, float y2) {
        BodyDef bodyBottom = new BodyDef();
        bodyBottom.type = BodyDef.BodyType.StaticBody;

        bodyBottom.position.set(0, 0);
        FixtureDef fixtureDefBottom = new FixtureDef();
        fixtureDefBottom.filter.maskBits = EntityConstants.PHYSICS_ENTITY;

        EdgeShape edgeShape = new EdgeShape();
        edgeShape.set(x1, y1, x2, y2);
        fixtureDefBottom.shape = edgeShape;

        bodyEdgeScreen = world.createBody(bodyBottom);
        bodyEdgeScreen.createFixture(fixtureDefBottom);
        edgeShape.dispose();
    }

    @Override
    public void render() {
        clearScreen();
        camera.update();
        deltaTimeSum += Gdx.graphics.getDeltaTime();

        world.step(1f / 60f, 6, 2);

        body.applyTorque(torque, true);

        updateSprite(spritePlayer, body);

        batch.begin();

        if (deltaTimeSum > 0.3) {
            deltaTimeSum = 0;
            this.createCollectable1s(deltaTimeSum);
        }

        if (drawSprite) {
            drawSpriteToBatch(batch, spritePlayer);
            updateCollectables1(Gdx.graphics.getDeltaTime());
        }

        batch.setProjectionMatrix(camera.combined);

        font.draw(batch, "FPS: " + Gdx.graphics.getFramesPerSecond(), -GAME_WIDTH / 2, GAME_HEIGHT / 2);
        font.draw(batch, "Elements: " + activeCollectable1s.size(), -GAME_WIDTH / 2, GAME_HEIGHT / 2 - 20);
        font.draw(batch, "SCORE: " + score, -GAME_WIDTH / 2, GAME_HEIGHT / 2 - 40);
        batch.end();

    }

    private void updateSprite(Sprite sprite, Body body) {
        sprite.setPosition(Utility.convertToPixel(body.getPosition().x) - sprite.getWidth() / 2, (Utility.convertToPixel(body.getPosition().y)) - sprite.getHeight() / 2);
        sprite.setRotation((float) Math.toDegrees(body.getAngle()));

    }

    private void clearScreen() {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }

    private void drawSpriteToBatch(SpriteBatch batch, Sprite sprite) {
        batch.draw(sprite, sprite.getX(), sprite.getY(), sprite.getOriginX(), sprite.getOriginY(), sprite.getWidth(), sprite.getHeight(), sprite.getScaleX(), sprite.getScaleY(),
                sprite.getRotation());

    }

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

    @Override
    public boolean keyDown(int keycode) {
        return false;
    }

    @Override
    public boolean keyUp(int keycode) {

        if (keycode == Input.Keys.RIGHT)
            body.applyForceToCenter(10f, 0f, true);
        if (keycode == Input.Keys.LEFT)
            body.applyForceToCenter(-10f, 0f, true);

        if (keycode == Input.Keys.UP)
            body.applyForceToCenter(0f, 10f, true);
        if (keycode == Input.Keys.DOWN)
            body.applyForceToCenter(0f, -10f, true);

        // On brackets ( [ ] ) apply torque, either clock or counterclockwise
        if (keycode == Input.Keys.RIGHT_BRACKET)
            torque += 0.1f;
        if (keycode == Input.Keys.LEFT_BRACKET)
            torque -= 0.1f;

        // Remove the torque using backslash /
        if (keycode == Input.Keys.BACKSLASH)
            torque = 0.0f;

        // If user hits spacebar, reset everything back to normal
        if (keycode == Input.Keys.SPACE || keycode == Input.Keys.NUM_2) {
            body.setLinearVelocity(0f, 0f);
            body.setAngularVelocity(0f);
            torque = 0f;
            spritePlayer.setPosition(0f, 0f);
            body.setTransform(0f, 0f, 0f);
        }

        if (keycode == Input.Keys.COMMA) {
            body.getFixtureList().first().setRestitution(body.getFixtureList().first().getRestitution() - 0.1f);
        }
        if (keycode == Input.Keys.PERIOD) {
            body.getFixtureList().first().setRestitution(body.getFixtureList().first().getRestitution() + 0.1f);
        }
        if (keycode == Input.Keys.ESCAPE || keycode == Input.Keys.NUM_1)
            drawSprite = !drawSprite;

        return true;
    }

    @Override
    public boolean keyTyped(char character) {
        return false;
    }

    @Override
    public boolean touchDown(int screenX, int screenY, int pointer, int button) {

        Vector3 touchPosition = new Vector3();

        camera.unproject(touchPosition.set(screenX, screenY, 0));

        // calculate the normalized direction from the body to the touch position
        Vector2 direction = new Vector2(touchPosition.x, touchPosition.y);
        direction.sub(body.getPosition());
        direction.nor();

        direction.x *= -1;
        direction.y *= -1;

        float speed = 3;
        body.setLinearVelocity(direction.scl(speed));

        return true;
    }

    @Override
    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
        return false;
    }

    @Override
    public boolean touchDragged(int screenX, int screenY, int pointer) {
        return false;
    }

    @Override
    public boolean mouseMoved(int screenX, int screenY) {
        return false;
    }

    @Override
    public boolean scrolled(int amount) {
        return false;
    }

    public void updateCollectables1(float delta) {
        Collectable1 item;
        int len = activeCollectable1s.size();
        for (int i = len; --i >= 0;) {
            item = activeCollectable1s.get(i);
            updateSprite(item.getSprite(), item.getBody());
            drawSpriteToBatch(batch, item.getSprite());
        }
    }

    public void createCollectable1s(float delta) {
        Collectable1 item = collectable1Pool.obtain();

        int randomInt;
        if (random.nextInt(2) == 1) {
            randomInt = random.nextInt(GAME_WIDTH / 2);
        } else {
            randomInt = -random.nextInt(GAME_WIDTH / 2);
        }

        item.init(collectable1Image, randomInt, GAME_HEIGHT - 100, world);

        activeCollectable1s.add(item);
    }

}

Collectable1.java

package game.entities;

import game.utilities.Utility;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Pool.Poolable;

public class Collectable1 implements Poolable {

    public Vector2 position;
    public boolean alive;

    private Body body;
    private Sprite sprite;

    /**
     * Collectable1 constructor. Just initialize variables.
     */
    public Collectable1() {
        this.position = new Vector2();
        this.alive = false;
    }

    /**
     * Initialize the Collectable1. Call this method after getting a Collectable1 from the pool.
     * 
     * @param world
     */
    public void init(Texture img, float posX, float posY, World world) {
        position.set(posX, posY);
        alive = true;

        // Create sprite
        sprite = new Sprite(img);
        sprite.setPosition(posX, posY);

        // Create BodyDef
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyDef.BodyType.DynamicBody;
        bodyDef.position.set(Utility.convertToMeter(sprite.getX() + sprite.getWidth() / 2), Utility.convertToMeter(sprite.getY() + sprite.getHeight() / 2));

        // Create Body
        body = world.createBody(bodyDef);

        // Create Friction
        FixtureDef fixtureDef = new FixtureDef();

        PolygonShape shape = new PolygonShape();
        shape.setAsBox(Utility.convertToMeter(sprite.getWidth() / 2), Utility.convertToMeter(sprite.getHeight() / 2));

        fixtureDef.shape = shape;
        fixtureDef.density = 0.1f;
        fixtureDef.restitution = 0.5f;
        fixtureDef.filter.categoryBits = EntityConstants.PHYSICS_ENTITY;
        fixtureDef.filter.maskBits = EntityConstants.WORLD_ENTITY | EntityConstants.PHYSICS_ENTITY;

        body.createFixture(fixtureDef);

        shape.dispose();
    }

    /**
     * Callback method when the object is freed. It is automatically called by Pool.free() Must reset every meaningful field of this Collectable1.
     */
    @Override
    public void reset() {
        position.set(0, 0);
        alive = false;
    }


    public Body getBody() {
        return this.body;
    }

    public Sprite getSprite() {
        return this.sprite;

    }

}

Utility.java

package game.utilities;

public class Utility {

    public final static float PIXELS_TO_METERS = 100f;

    static public float convertToPixel(float value) {
        return value * PIXELS_TO_METERS;
    }

    static public float convertToMeter(float value) {
        return value / PIXELS_TO_METERS;
    }

}

EntityConstants.java

package game.entities;

public class EntityConstants {

    public final static short PHYSICS_ENTITY = 0x1;
    public final static short WORLD_ENTITY = 0x1 << 1;

}

Edit: Just in case you are interrested: After half a year I actually got the game done.

\$\endgroup\$
3
\$\begingroup\$

Unfortunately, I don't know libgdx, and cannot answer about your performance problem, but some issues with this code jump into the eye.

Public fields

public class MyGame extends ApplicationAdapter implements InputProcessor {
    SpriteBatch batch;
    Sprite spritePlayer;
    Texture img;

Public fields or fields with default visibility are a bad practice. Make these (and all others in all your classes) private and add getters and setters as necessary.

Inefficient if statements

@Override
public boolean keyUp(int keycode) {

    if (keycode == Input.Keys.RIGHT)
        body.applyForceToCenter(10f, 0f, true);
    if (keycode == Input.Keys.LEFT)
        body.applyForceToCenter(-10f, 0f, true);

    if (keycode == Input.Keys.UP)
        body.applyForceToCenter(0f, 10f, true);
    if (keycode == Input.Keys.DOWN)
        body.applyForceToCenter(0f, -10f, true);

The if statements above are inefficient, because the conditions are mutually exclusive (only one of them can be true), but all of them will be evaluated always. You could remove the inefficiency by using else if, but it will be more compact and readable to rewrite with a switch instead:

switch (keycode) {
    case Input.Keys.RIGHT:
        body.applyForceToCenter(10f, 0f, true);
        break;
    case Input.Keys.LEFT:
        body.applyForceToCenter(-10f, 0f, true);
        break;

This will eliminate some unnecessary evaluations, improving the performance of handling key presses, though this will not be noticeable at all.

Code duplication

While this is quite alright, it has some duplicate code:

    int randomInt;
    if (random.nextInt(2) == 1) {
        randomInt = random.nextInt(GAME_WIDTH / 2);
    } else {
        randomInt = -random.nextInt(GAME_WIDTH / 2);
    }

It's better to not repeat yourself:

    int randomHalfWidth = random.nextInt(GAME_WIDTH / 2);
    int randomInt = random.nextInt(2) == 1 ? randomHalfWidth : -randomHalfWidth;
\$\endgroup\$
  • 1
    \$\begingroup\$ It is actually quite common to have default-visibility on libGDX-specific elements. Many of the tutorials have it. I agree though that it's not the best practice. No setters should be necessary for it though. \$\endgroup\$ – Simon Forsberg Jan 26 '15 at 18:15
  • 1
    \$\begingroup\$ That's why I wrote to add as necessary, as opposed to add. Tutorials often don't bother to follow best practices. I see that even in the best of books. That's ok, when the examples want to focus on a specific detail, and don't necessarily aim to be exemplary. \$\endgroup\$ – janos Jan 26 '15 at 18:18
  • \$\begingroup\$ I normally never ever use variables without visibility modifiers. I really just had copy past that part from a tutorial and hadn't bothered to fix it yet :) \$\endgroup\$ – Markus Jan 27 '15 at 7:26
  • 1
    \$\begingroup\$ @Markus you'll get the most out of code review by first making the code as perfect as you possibly can. That way you can avoid trivial objections and get more relevant answers \$\endgroup\$ – janos Jan 27 '15 at 9:06

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.