2. Improving the game world using textures

In this post we will try and improve the World we made earlier, graphics wise. For this we have three methods: textures, cached textures and LibGDX' TiledMap. We will build this using the code we created last time.

The sprites we will use can be found here and will be placed in our Android project in assets/images/tiles.png.

LibGDX textures

In order to use textures we do not have to change much of our code, most of it is already there. For now I will just hard-code the coordinates to our textures. If we want to use textures we can combine LibGDX' Texture and TextureRegion classes (amongst others). This code should be fairly self-explanatory. Whereas we used a ShapeRenderer in my last post, we will now use a SpriteBatch, which is its more advanced brother. SpriteBatches have the ability to use textures and sprites and draw those onto our screen, though it is quite heavy, so using only one is recommended. Do not forget to turn GL20 on in your Android/desktop Main class.

public class World {

    //size of the Tiles in pixels, required for drawing
    public static final int TILESIZE = 16;

    public int width; //amount of blocks per row
    public int height; //amount of blocks per column

    //(col, row) => (row * width + col)
    //(x, y)     => (y * width + row)
    public Tile[] mapData; 

    //textures and regions
    Texture texture;
    TextureRegion road, tree, destination;
    SpriteBatch batch = new SpriteBatch();

    /**
     * Constructor
     */
    public World()
    {
        loadMap("map1");    

        //load textures and regions
        this.texture = new Texture(Gdx.files.internal("images/tiles.png"));
        this.road = new TextureRegion(texture, 4*16, 2*16, 16, 16);
        this.tree = new TextureRegion(texture, 1*16, 0*16, 16, 16);
        this.destination = new TextureRegion(texture, 1*16, 9*16, 16, 16);
    }

    /**
     * Loads a text file containing a world
     */
    private void loadMap(String mapName) {
        ArrayList<String>; worldTextrows = new ArrayList<String>();

        // The InputStream opens the resourceId and sends it to the buffer
        FileHandle worldFile = Gdx.files.internal("data/maps/" + mapName + ".txt");
        BufferedReader worldFileReader = new BufferedReader(worldFile.reader());

        try {
            String worldRow = worldFileReader.readLine();
            this.width = worldRow.length() / 2 + 1;
            while (worldRow != null) {
                worldTextrows.add(worldRow);
                if (worldRow.length() / 2 + 1 != width)
                    Gdx.app.error("tag", "The length of the rows are not equal.");
                worldRow = worldFileReader.readLine();
            }

            // Close the InputStream and BufferedReader
            worldFileReader.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

        this.height = worldTextrows.size();
        this.mapData = new Tile[this.width*this.height];    

        //create the tiles and enter them into our mapData array
        String[] worldTemp = new String[this.width];
        for (int row = 0; row < this.height; row++)
        {
            worldTemp = worldTextrows.get(row).split(&quot; &quot;);
            for (int col = 0; col < this.width; col++)
            {
                TileType type = TileType.fromInteger(Integer.parseInt(worldTemp[col]));

                this.mapData[row * this.width + col] = new Tile(type, col, row);
            }
        }
    }

    Tile tile; //placed here to avoid garbage collection
    /**
     * Draws our tiles one by one
     * @param camera
     */
    public void render(OrthographicCamera camera)
    {
        batch.setProjectionMatrix(camera.combined);
        batch.begin();  

        for(int x = 0; x < width; x++) {
            for(int y = 0; y < height; y++) {   
                //grab tile
                tile = mapData[y * width + x];

                //draw tile
                if(tile.getType() == TileType.TREE) {
                    batch.draw(tree, tile.getX(), tile.getY());
                } else if(tile.getType() == TileType.ROAD) {
                    batch.draw(road, tile.getX(), tile.getY());
                } else {
                    batch.draw(destination, tile.getX(), tile.getY());
                }
            }
        }
        batch.end();
    }
}

Some benchmarks

Mind you, the world consists of 68x34 (= 2312) tiles where each tile is 16x16px. All tests do 1000 frames and are ran three times.

HTC Desire HD
time/frame in ms 6 6 6
frames per second 143 147 150
Desktop

This one caps at around 60 fps due to some configurations in LibGDX, it is possible to turn this off, resulting in well over 1800 frames but it is not necessary here. 60 is the target.

time/frame in ms 16 16 16
frames per second 62 61 60
First generation ASUS Transformer
time/frame in ms 6 6 7
frames per second 152 147 140

All the bench marks put us at above 60 fps, which is nice. We would however like to get this as high as possible so we have some power left to calculate more important stuff. Which is why we shall take a look at cached textures next.

LibGDX cached textures

All those ifs and checks are kind of useless for a group of sprites that do not change. It would be nice if we could preload all this, store it in one large "sprite" and just call that one. Luckily for us, LibGDX comes with a SpriteCache, which is almost like a SpriteBatch but it caches the sprites (which have to be loaded beforehand). So, how do we do that?

We again have to change some minor stuff in our World class. Luckily it is just a matter of adding a bit and removing a bit. Our first step is to create a new SpriteCache and an integer called cacheID which we will use give a value of 0, not that important right now.

The code is rather hacky and ugly but it will suffice for now.

public class World {

    //size of the Tiles in pixels, required for drawing
    public static final int TILESIZE = 16;

    public int width; //amount of blocks per row
    public int height; //amount of blocks per column

    //(col, row) => (row * width + col)
    //(x, y)     => (y * width + row)
    public Tile[] mapData; 

    //textures and regions
    Texture texture;
    TextureRegion road, tree, destination;

    SpriteCache cache;
    int cacheID = 0;

    /**
     * Constructor
     */
    public World()
    {
        loadMap("map1");    

        //load textures and regions
        this.texture = new Texture(Gdx.files.internal("images/tiles.png"));
        this.road = new TextureRegion(texture, 4*16, 2*16, 16, 16);
        this.tree = new TextureRegion(texture, 1*16, 0*16, 16, 16);
        this.destination = new TextureRegion(texture, 1*16, 9*16, 16, 16);

        //create the entire map and put it in our cache
        cache = new SpriteCache(height*width, true);
        cache.beginCache();

        //this is equal to our earlier render method
        for(int row = 0; row < height; row++)
            for(int col = 0; col < width; col++) {
                TileType type = mapData[row * width + col].getType();
                if(type == TileType.ROAD) {
                    cache.add(road, col*TILESIZE-(TILESIZE/2), row*TILESIZE-(TILESIZE/2), TILESIZE, TILESIZE);
                } else if(type == TileType.TREE)
                {
                    cache.add(tree, col*TILESIZE-(TILESIZE/2), row*TILESIZE-(TILESIZE/2), TILESIZE, TILESIZE);
                } else if(type == TileType.DESTINATION)
                {
                    cache.add(destination, col*TILESIZE-(TILESIZE/2), row*TILESIZE-(TILESIZE/2), TILESIZE, TILESIZE);
                }
            }
        cacheID = cache.endCache();
    }

    /**
     * Loads a text file containing a world
     */
    private void loadMap(String mapName) {
        //removed to save space
    }

    /**
     * Draws our tiles one by one
     * @param camera
     */
    public void render(OrthographicCamera camera) {
        cache.setProjectionMatrix(camera.combined);
        cache.begin();
        cache.draw(cacheID); //call our cache with cache ID and draw it
        cache.end();
    }
}

Some benchmarks

All tests do 1000 frames and are run three times.

HTC Desire HD
time/frame in ms 2 2 2
frames per second 498 350 378
Desktop

Somehow it does not cap at 60 this time, even though I have not tweaked the configurations. If I do change these then I get results that go well beyond 25000 fps

time/frame in ms ~0.08 ~0.07 ~0.08
frames per second 12048 14084 12048
First generation ASUS Transformer
time/frame in ms ~0.7 ~0.7 ~0.7
frames per second 1398 1436 1379

Look at that! This is more like it, but how is this possible? Well, firstly we removed two loops which looped through 2312 values every single time. That looping wastes valuable processing time. Secondly, the textures are stored in cache which means that they do not have to be sent to the GPU every single time.

LibGDX TiledMap

Creating maps this way is still quite cumbersome. LibGDX has the ability to use TiledMap, which loads and draws maps created through Tiled (a piece of software that allows you to create large and sophisticated tile maps though clicking, dragging, filling and such). I have tested this and though creating maps like this is awesome, the drawing performed rather poorly. So I will not go into this in this post, but I may pick this up sometime later again.

Rounding up

What have we discovered in this post. For starters: we can conclude that our beginner's approach to drawing our game world was not that good, not at all actually. Due to it having multiple loops it ate valuable processing time. (We can however eliminate one loop considering we do not actually use the x and y created inside that loop! I quickly tested it and it resulted in a boost of 10-15% in fps.)

We also saw that it is rather easy to switch out our colored tiles by textured tiles and even gain some more fps. In other to push that fps even further, we took a look at cached textures, which allowed us to load the textures once and then draw them very quickly.

comments powered by Disqus