Making a LibGDX Roguelike Survival Game Part 2 – Adding a simple Island #gamedev

The tiles used for this game are available here Open Game Art tile set, Open Game Art has some really great tile sets worth using; especially when you are prototyping. This is an example from the site showing what can be achieved with this tile set:

[ Full source code for this tutorial ]

For this part of the tutorial we will review the new classes, Entity, Chunk, Tile and Island, these are used to generate a simple island/ map which can be rendered to the screen.

Entity
The key variables for the entity class are as follows:

public Vector2 pos;
public Texture texture;
public float width;
public float height;

Almost all objects have position, width, height and a texture, this is enough information to draw an image to the screen at a certain and co-ordinate. The class has a simple draw function which can be called by classes we create that extend Entity, we can override this method if more logic etc is needed in the draw function:

public void draw(SpriteBatch batch){
  batch.draw(texture, pos.x, pos.y, width, height);
}

An Enums class was created to hold all the enumerators we will need, at the moment we only need some TILETYPE‘s so we can set the type of our tiles, grass, water and cliff.

The three new classes for defining, generating and drawing the island are added to a new package named “map”. Packages are used to organise related classes (like a folder), even a small game can become over loaded with classes quite quickly.

Tile.java
This holds information on an individual tile (square space) which extends Entity; giving access to all of the properties of that class:

public class Tile extends Entity {

When we initiate a new tile we pass in its location, size, type and texture, the size is basically the scale, if we decide on a smaller/larger tile size later we can easily experiment changing only the size. Calling super() will initiate the Entity (calls the Entity public function).

public Tile(float x, float y, int size, TILETYPE type, Texture texture){
  super();
  pos.x = x*size;
  pos.y = y*size;
  this.size = size;
  this.texture = texture;
  this.col = (int) x;
  this.row = (int) y;
  this.type = type;
  this.code = "";
}

There are a number of boolean functions that make testing tiles more readable e.g tile.is_grass() or tile.is_water(), is_passable(), this becomes useful when we have a lot of code that checks a tile is passable, changes to the passable logic can be maintained in place e.g:

public boolean is_grass() {
  return type == TILETYPE.GRASS;
}

Chunk.java
A chunk will hold an array of arrays of tiles, it can useful to split the chunk into rows for rendering order and for selecting tiles, you do not have to do this and could just have one big array of tiles; ArrayList, the structure:

Chunk
        Row
                Tile, Tile, Tile
        Row
                Tile, Tile, Tile
        Row
                Tile, Tile , Tile

Chunk public constructor accepts the number of rows, columns and the size of the tiles (tiles are square):

public Chunk(int number_rows, int number_cols, int tile_size ){
  tiles = new ArrayList<ArrayList>();
  this.number_rows = number_rows;
  this.number_cols = number_cols;
  this.tile_size = tile_size;
}

We have some useful functions in the chunk class, both require a row and column integer; get_tile returns a tile and get_tile_code returns the code for a tile. Later when we allow interaction with tiles or need to check the tile the hero is on we can calculate the row and column by the cursor or hero x and y positions.

Tile Code
Creating a code to describe a tiles neighbours allows us to make graphic and logic decisions. The centre tile show below has a code of “111000000”, we read the code left to right one row at a time, the three tiles above are all passable so get set to 1, the cliffs and water are all 0’s.

tilecode

This method can be repeated to achieve all sorts, for example you could find all grass touching water and turn it into sand.

Island.java
This class will create a new chunk and populate it with tiles, there is logic to first fill the chunk with water tiles, then find the centre and create a grass island. There is some code present which can be refactored and moved into other classes later. Often the first draft or prototype code is messy especially in a game jam or challenge.

To hide some of the complexity the public constructor for the island class calls three functions:

public Island(){
setup_images();
setup_tiles();
code_tiles();
}

* Not everyone will like the naming conventions I use, I work with Ruby, Go, PHP, VB.net, C# and Java so use a mixture sorry! :Fixed in part 5 *

The images should really be loaded in another class, this will be address in the next part of this series. We setup our images as LibGDX Textures. Further information on loading files available on the wiki, when you are writing a game for mobile or HTML you will need to look at the File Handler, we can get away with a relative path for our images though:

grass_01 = new Texture("8x8/grass/grass_01.png");

The next step is to create a chunk and fill it with tiles. For now we will go with a chunk size of 33 (a random number I selected, we can revisit this), the tiles are size 8, to create an island we:

  • Generate a random number 5 to 8 for half the height and width in tiles
  • Find the centre row and column
  • Calculate first and last row and columns

This defines a randomly (within set bounds) sized square which we can place in the middle of the chunk.

chunk = new Chunk(33,33, 8);

int current_row = 0;
int rng_w = MathUtils.random(5,8);
int rng_h = MathUtils.random(5,8);

int centre_tile_row = chunk.number_rows / 2;
int centre_tile_col = chunk.number_cols /2;
int first_tile_row = centre_tile_row - (rng_h);

int max_row = centre_tile_row + rng_h;
int min_row = centre_tile_row - rng_h;
int max_col = centre_tile_col + rng_w;
int min_col = centre_tile_col - rng_w;

To create all the tiles we can use a two for loops, for example:

int rows = 2;
int columns = 2;

for (int r = 0; r < rows; r++){ // Loop for rows
  for (int c = 0; c < columns; c++){ // loop for columns
    // CREATE TILES HERE
    System.out.println("Row: " + r + " Column: " + c);
  }
}

This would output (2 x 2 map, 8 tiles):

Row: 0 Column: 0
Row: 0 Column: 1
Row: 1 Column: 0
Row: 1 Column: 1

The map generation works the same, its starts by creating a new tile inside of second for loop and making that WATER by default, random_water() is called to set the texture, this makes the map more interesting by varying the water image used, as the random int is 0 to 20 and only 2, 3, 4 select alternative images most water tiles will be water_01 texture.

// Create TILE
Tile tile = new Tile(col, row, chunk.tile_size, TILETYPE.WATER, random_water());

// Random Texture
private Texture random_water(){
Texture water;

int tile = MathUtils.random(20);
switch (tile) {
    case 1: water = water_01;
        break;
    case 2: water = water_02;
        break;
    case 3: water = water_03;
        break;
    case 4: water = water_04;
        break;
    default: water = water_01;
        break;
    }
    return water;
}

When looping through the tiles the max and min columns and row variables can be used to check if the current row and column are inside of values, if they are then the tile can be changed to GRASS

if(row > min_row && row < max_row && col > min_col && col < max_col){
    tile.texture = random_grass();
    tile.type = TILETYPE.GRASS;
    ..........
}

We store the lowest row of grass tiles, if we are currently processing those they we can make the type a CLIFF:

if(row == first_tile_row + 1){
  tile.texture = cliff;
  tile.type = TILETYPE.CLIFF;
} else {
  // Chance to add trees etc
}

We have to use first_tile_row + 1 as we have row > min_row and not  row >= min_row.

The code for adding the tiles to a row and adding the rows to the chunk works like this:

if current row
        add new tile to the row array
if current row and column are the final then add the row array to the chunk
else
        set current row number
        add row to chunk
        clear array row (new array)
        add tile to array row

Here is the actual code that builds the array of array of tiles:

// ADD TILE TO CHUNK
if(current_row == row){
  // Add tile to current row
  chunk_row.add(tile);

  // Last row and column?
  if (row == chunk.number_rows - 1 && col == chunk.number_cols - 1){
    chunk.tiles.add(chunk_row);
  }
} else {
  // New row
  current_row = row;

  // Add row to chunk
  chunk.tiles.add(chunk_row);

  // Clear chunk row
  chunk_row = new ArrayList();

  // Add first tile to the new row
  chunk_row.add(tile);
}

Now that the tiles all of the tiles exist the tile codes can be calculated,

for(ArrayList row : chunk.tiles){
  for(Tile tile : row){
    // Check all surrounding tiles and set 1 for pass 0 for non pass
    // 0 0 0
    // 0 X 0
    // 0 0 0

    int[] rows = {1,0,-1};
    int[] cols = {-1,0,1};

    for(int r: rows){
      for(int c: cols){
        tile.code += chunk.get_tile_code(tile.row + r, tile.col + c);
        update_image(tile);
      }
    }
  }
}

As we only have one chunk at the moment we loop all tiles in the array’s of rows, when we have multiple chunks we would need another for loop to do this for each chunk. For each tile we need to get all of the neighbouring tiles, int[] rows = {1,0,-1} these numbers are used to represent one row above, same row and the row below the current, we have the came for columns, two for loops allows us to get a reference to all eight surrounding tiles plus itself.

Once the Tile object has a code we can check if it requires an additional image, we do this by checking if the tiles code matches any in the arrays created in the earlier.

if(Arrays.asList(a_grass_left).contains(tile.code)){
  tile.secondary_texture = grass_left;
} else if (Arrays.asList(a_grass_right).contains(tile.code)){
  tile.secondary_texture = grass_right;
....

When we allow non square island shapes these arrays will be extended and more arrays needed for other edges that will be required.

Going back to the main class we need to make some changes and use this new Island class to create our map.

// Lower speed to 1
int speed = 1;

// A new Class variable Island
Island island;

// Render Loop, clear BLACK
Gdx.gl.glClearColor(0, 0, 0, 0);

// draw code - loop chunks and draw tiles
batch.begin();
// Draw all tiles in the chunk / chunk rows
for(ArrayList row : island.chunk){
  for(Tile tile : row){
    batch.draw(tile.texture, tile.pos.x, tile.pos.y, tile.size, tile.size);
    if (tile.secondary_texture != null) batch.draw(tile.secondary_texture, tile.pos.x, tile.pos.y, tile.size, tile.size);
  }
}
batch.end();

There is no culling, all tiles will be rendered, this can be addressed when there are more chunks. With the changes the main gameclass running the app should produce a map that you can move around using the arrow keys or WASD:

island

In part 3 we will refactor the images into a new class and add our hero.

Remember to check out the full source code: [ Github Repository ]

Advertisements

One thought on “Making a LibGDX Roguelike Survival Game Part 2 – Adding a simple Island #gamedev

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s