Monday, February 13, 2012

How to draw isometric tile map

Drawing an isometric map is not that different from a regular tile map. Granted there are a bit more going on when calculating where to draw the next tile on the screen, but traversing the map is the same.

On a regular tile map, you start by drawing the tile furthest away so that you don't overwrite tiles that have already been drawn. It's the same with isometric. In your head, keep thinking of the map as a 2 dimensional array where you start by drawing all tiles on the x axis and then increasing y and draw all tiles along the x axis on the line. It's the same thing you do with isometric. There are other ways, but this is the easiest one, at least I think so. The other one involves even and odd lines and stepping through the map in a strange way.

Now you need to draw them on the screen, and that is where the tricky part is. I have added my code to draw the floor, see it below. There are surprisingly..... comments in it, how did that happen?

The code that implements this.
private void drawFloor(){

    // Get player position, only used if player is always at the center of the screen
    int mapDrawCenterX = player.getMapPositionX();
    int mapDrawCenterY = player.getMapPositionY();

    // how many tiles to draw on either side of the player   
    int startMapX = mapDrawCenterX - Globals.DRAW_TILES_X;
    int startMapY = mapDrawCenterY - Globals.DRAW_TILES_Y;
    int endMapX = mapDrawCenterX + Globals.DRAW_TILES_X +1; // +1 is to create a center tile for player
    int endMapY = mapDrawCenterY + Globals.DRAW_TILES_Y +1; // +1 is to create a center tile for player

    // the first tile to draw
    int mapX = startMapX;
    int mapY = startMapY;

    // displacing the drawing area so that it's don't start at the top left of the screen
    int drawStartX = 512;
    int drawStartY = -250;

    int drawX = drawStartX;
    int drawY = drawStartY;

    int linesDone = 0;
    boolean done = false;
    while (!done){

        // Check if this tile is inside the map, check if the drawing coordinate is inside screen
        if ((map.isInsideMap(mapX, mapY)) && (drawX <= 1024 || drawY <= 768) && ((drawX + Globals.FLOOR_TILE_SIZE_X + Globals.FLOOR_TILE_PADDING_X > 0) && (drawY + Globals.FLOOR_TILE_SIZE_Y + Globals.FLOOR_TILE_PADDING_Y > 0))){

            // Get the tile and check if there is an image to draw. -1 means nothing there.
            Tile tile = map.getTile(mapX, mapY);

            if (tile.getImageIndex(Tile.FLOOR) != -1){
                Image floor = map.floorImageList.getImage((tile.getImageIndex(Tile.FLOOR))).getImage();

                // Get lightmap corners for this tile
                float[][] lightMapColor = lightMap.getLightCorner(mapX, mapY);
                   
                // Set image corners to the lightmap corners.
                floor.setColor(0, lightMapColor[0][0], lightMapColor[0][1], lightMapColor[0][2], 1.0f);
                floor.setColor(1, lightMapColor[1][0], lightMapColor[1][1], lightMapColor[1][2], 1.0f);
                floor.setColor(2, lightMapColor[2][0], lightMapColor[2][1], lightMapColor[2][2], 1.0f);
                floor.setColor(3, lightMapColor[3][0], lightMapColor[3][1], lightMapColor[3][2], 1.0f);

                // Finally draw image
                floor.draw(drawX, drawY);
            }
        }

        // step along the x axis of the map
        mapX++;

        // Move drawing "pointer" to the next tile, half tile length on x and half tile height on y
        drawX += (Globals.FLOOR_TILE_SIZE_X + Globals.FLOOR_TILE_PADDING_X)/2;
        drawY += (Globals.FLOOR_TILE_SIZE_Y + Globals.FLOOR_TILE_PADDING_Y)/2;

        // Nothing more on this x line on map, starting on next line
        if (mapX >= endMapX){

            linesDone++;
            mapX = startMapX;
            mapY++;

            // black magic moving the drawing pointer to the start of the next line ;-)
            drawX = drawStartX - (((Globals.FLOOR_TILE_SIZE_X+Globals.FLOOR_TILE_PADDING_X)/2)*linesDone);
            drawY = drawStartY + (((Globals.FLOOR_TILE_SIZE_Y+Globals.FLOOR_TILE_PADDING_Y)/2)*linesDone);
        }

        //Check if we are done and end the loop
        if (mapY >= endMapY)
            done = true;
    }
}


There are probably a lot of optimizations that can be done to it, I haven't looked into it yet as the game was running fast enough as it is. Besides, don't spend time optimizing stuff before it's necessary, and use a profiler to identify bottlenecks.

There's some stuff there you cant see, like the tile padding. I use that as my tiles don't connect directly, there is a 2 pixels gap between them on the x axis so that the tile below fit's in it. See pictures below.

The tiles are 62*32 pixels.


So when adding more rows of tiles they fit into the 2 pixel gap on the x axis.

The 2 pixel gap is not something I came up with, it's a friend of mine. He's a graphics artist and it all adds up in the end.

Hope that helped, please ask if you have any more questions.

2 comments:

  1. where are your classes? this is to confusing to edit and change for myself if I don't know where the classes are

    ReplyDelete
    Replies
    1. What classes are you looking for?

      Most of the stuff here is something you have to write for your self. The map contains a double array of tile and the tiles contain the information you need for your map.

      You don't need all that, to get started.

      Just create a double array of int and assign some random numbers to it let's say -1 to 3. -1 = no image, 0 = floor image 1, 1 = floor image 2, and so on.

      So where I do this : Tile tile = map.getTile(mapX, mapY); you just take the value from your double array. Where I check if (tile.getImageIndex(Tile.FLOOR) != -1){ you just use the number you just extracted from your double array.

      Then replace the Globals class I use with some hard coded image sizes and padding and you should be ready to go.

      Delete