1. new project & the game world; a beginner's approach

Welcome back!

In my last post I explained what this project/game was about, in case you forgot: a combination of tower defense, RTS and RPG. In this post and in the next 1 or 2 we will take a look at the game world, what it is supposed to look like and how to realize that.

Setting up

Before we can begin we have to start a new project, luckily LibGDX has a program for that. After downloading the latest nightlies we open gdx-setup-ui.jar, click create. In the following screen we enter a Name (let's name it Prototype), Package (me.emptymind.prototype), game class (Prototype.java) and choose a destination. I chose to add a desktop, html and iOS project. No third-party packages are being used for now and we can create the project. When opening this in Eclipse we are good to go.

More info on how to set up your project

Where to start?

Everything in this game takes place in a world, so it seems only logical to start with that. This specific world should consist of trees, path and destinations, nothing major for now. The code I wrote has been changed quite a lot and I will try to recall stuff as good as possible (screw my lack of dedication to write logs). The first step I took was the easiest (and worst one in terms of efficiency) but it allowed for a rapid prototype.

A beginner's approach to creating a world

The world will be in 2D and we will look at it top-down, so we use an orthographic camera (LibGDX page with more information). The easiest approach right now is using a map of square tiles, which we can load from a text file. Because it is 2D these tiles can fit in a 2D array perfectly. When drawing we just loop through the array while drawing each tile. To save some time we will give a color to a tile, instead of a sprite: Green = tree, Gray = path, Black = enemy, Red = destination for enemies

Steps to take:

  • Add a new Screen to our Main class
  • Create a new Orthographic camera
  • Create a World class that holds information about map size and its tiles
  • Create a Tile class to hold information about a Tile
  • Create a simple map in a text file
  • Create a loader for the text map
  • Draw the world onto our screen

Add a new Screen to our Main class

Before we can add a new screen to our Main class (Prototype.java) we have to change ApplicationListener to Game and remove all code but the create() method. It also makes sense to actually have a Screen. So let's create that and call it GameScreen.java and let it implement Gdx' Screen class.

Create a new Orthographic camera

The next step is to create an Orthographic camera, which is rather straightforward. We will give it a width of 800px and height of 480px and set yDown. We only need show and render for now, so clean up the other so they don't clog your class.

Why yDown? I have been using the upper left corner with y pointing down ever since I started using XNA and kind of stuck with it. If you prefer y to point up, feel free to change it, but remember all my code uses yDown.

Create a World class that holds information about map size and its tiles

Now, onto the World class. A World has a width and a height, in terms of Tiles, which will be set by the method that reads our text file. It will also hold an 1D array which holds all our tiles. Why 1D when I mentioned 2D earlier? It is a simple performance improvement where we can get the Tile at (x, y) with (y * width + x). This is all for now, we first have to implement the Tile class.

Create a Tile class to hold information about a Tile

This class contains a little more information, like the color, the type of Tile and its location. For the TileType we use an enum, and the code is pretty straightforward. The code is posted below, right with all the other code.

Create a simple map in a text file

See the map1.txt file below

Create a loader for the text map

In this part we read every character other than a space and convert it to a Tile. You can look at the loadMap() function in the World class.

Draw the world onto our screen

This part is just a matter of looping through all the Tiles and drawing them. Because we only use colors and squares right now I decided to use LibGDX' ShapeRenderer. It does come at a cost: efficiency, it is quite costly.

The result

Simple world example

public class Prototype extends Game {

    @Override
    public void create() {  
        //set our GameScreen as our active screen
        setScreen(new GameScreen());
    }
}
public class GameScreen implements Screen {

    private OrthographicCamera camera; //2D camera  
    private World world;

    @Override
    public void show() {
        this.camera = new OrthographicCamera();
        this.camera.setToOrtho(true, 800, 480);

        world = new World();
    }

    @Override
    public void render(float delta) {
        //clear screen
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

        world.render(camera);
    }


    @Override public void resize(int width, int height) {}
    @Override public void hide() {}
    @Override public void pause() {}
    @Override public void resume() {}
    @Override public void dispose() {}

}
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; 

    private ShapeRenderer debugRenderer = new ShapeRenderer();

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

    /**
     * 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(" ");
            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 so to avoid garbage collection
    /**
     * Draws our tiles one by one
     * @param camera
     */
    public void render(OrthographicCamera camera)
    {
        debugRenderer.setProjectionMatrix(camera.combined);
        debugRenderer.begin(ShapeType.Filled);  

        for(int x = 0; x < width; x++)
        {
            for(int y = 0; y < height; y++)
            {   
                //grab tile
                tile = mapData[y * width + x];
                //set tile's color
                debugRenderer.setColor(tile.getColor());
                //draw tile
                debugRenderer.rect(tile.getX(), tile.getY(), TILESIZE, TILESIZE);

            }
        }
        debugRenderer.end();
    }
}
public class Tile {  
    TileType type;
    Color color;
    int x, y;

    public static enum TileType {
        TREE, ROAD, DESTINATION;

        public static TileType fromInteger(int id)
        {
            switch(id)
            {
                case 0: return TREE;
                case 1: return ROAD;
                case 2: return DESTINATION;
                default: return TREE;
            }
        }
    }

    public Tile(TileType type, int x, int y)
    {
        this.type = type;
        this.x = x;
        this.y = y;

        if      (type == TileType.TREE)       { this.color = Color.GREEN; } //trees - solid
        else if (type == TileType.ROAD)        { this.color = Color.DARK_GRAY; } //floor
        else if (type == TileType.DESTINATION){ this.color = Color.RED; } //target area
        else {                                     this.color = Color.WHITE; }
    }


    /*** Getters ***/
    public TileType getType()
    {
        return this.type;
    }

    public Color getColor()
    {
        return color;
    }

    /**
     * Returns the position of this tile in the world
     * @return
     */
    public float getX()
    {
        return this.x * World.TILESIZE;
    }
    public float getY()
    {
        return this.y * World.TILESIZE;
    }

    /**
     * Returns whether or not a Tile is passable
     * @param type
     * @return
     */
    public static boolean isPassable(TileType type)
    {
        if(type != Tile.TileType.TREE)
            return true;
        return false;
    }
}

map1

Some benchmarks

Mind you, the world consists of 68x34 (= 2312) tiles where each tile is 16x16px.

HTC Desire HD
frames 100 1000
time/frame (ms) 17 (56 fps) 16 (61 fps)
Desktop
frames 100 1000
time/frame (ms) 5 (175 fps) 16 (60 fps)
First generation ASUS Transformer
frames 100 1000
time/frame (ms) 17 (55 fps) 14 (71 fps)

Closing up

In this post we have created a very basic version of the game world using a beginner's approach. In the next post we will take a closer look at some other methods to draw our World, like with LibGDX textures, cached textures and a TiledMap.

comments powered by Disqus