January 12, 2016

Android 2D RPG - Part 7: A Game Map

Hello readers! Sorry this post took a little to prepare. Truth be told, I was struggling to decide what to add next to our game... So, after a while, I decided it was time to add a game map!!! Woooo!!! xD

Fair warning tho: this post might be a bit long, and will require you to get used to new software, which is actually pretty cool and easy to use ^^.

Note: I'm adding a game map, not a World Map. In terms of Pokémon games, we're talking about those that change when you enter a building or switch zones, not the view you get when you use Fly or check the map, that'd be a World Map.

First of all, I'm going to be using maps generated with Tiled Map Editor, cause, imho, there's nothing better. You can download it here, and they provide a nice (if a little long) manual that you can read here.

I won't explain step by step how to use Tiled, but I'll show you how to make a sample map for your game with a sort of picture tutorial. First of all, you'll need a tileset, you can use this one.


Ok, here's the basics to get the data we'll need.
  1. Choose a map size and base tile size
  2. Add a tileset
  3. Place tiles on the map layers (you can add as many layers as you want)
  4. Export your map as "Flare map files (*.txt)"
Few things to mention here... I'll be using 3 (for now) layers. 1st one is for terrain, this layer is going to be completely filled. 2nd layer is for objects, squares where our hero won't be able to step on (very important to add it now, since we'll need this for path-finding). And last but not least, the 3rd layer is for the roof part of objects, stuff that will display over other objects and even our hero. Here's the tut!









On our exported .txt we'll have something like this, that's the data we'll need.


So, back to Android Studio now, first thing we'll need to do, is add our tileset image to our /res/drawable directory. Now, it's time to make a new Java Class, name it GameMap and this will be, well, our game map class, duh! :P

GameMap.java
package ve.com.biocraft.biocraft;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.Log;

public class GameMap {

    private static final String TAG = GameMap.class.getSimpleName();

    private Bitmap bitmap;      // The image of our map's tiles
    private int[] terrain;      // Array of terrain for background buffer
    private int[] object;       // Array of objects for background buffer (hero can't move through this)
    private int[] roof;         // Array of elements for the top buffer
    private int width;          // Numbers of tiles in a row from bitmap
    private int square;         // Screen square size
    private Bitmap buffer;      // Buffer for background elements
    private Bitmap roofBuffer;  // Buffer for top elements (covers bottom, hero, NPCs, etc)

    public GameMap(Bitmap bitmap, int[] terrain, int[] object, int[] roof, int width, int square) {
        this.bitmap = bitmap;
        this.terrain = terrain;
        this.object = object;
        this.roof = roof;
        this.width = width;
        this.square = square;
        fillBuffers();
        Log.d(TAG, "GameMap: Map ready!");
    }

    private void fillBuffers() {
        // We crate 2 image buffers to be drawn on each render
        // One for background elements (buffer)
        // Another for top elements (roofBuffer)
        buffer = Bitmap.createBitmap(width * square, (terrain.length / width) * square, Bitmap.Config.ARGB_8888);
        roofBuffer = Bitmap.createBitmap(width * square, (terrain.length / width) * square, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(buffer);
        Canvas roofCanvas = new Canvas(roofBuffer);
        // Draw each array on the corresponding buffer, useful if there's need to add more layers of elements
        draw(terrain, canvas);
        draw(object, canvas);
        draw(roof, roofCanvas);
    }

    private void draw(int[] ints, Canvas canvas) {
        // Draw an array of sprite ids into a canvas
        int spriteSize = (bitmap.getWidth() / 8);
        for (int i = 0; i < (ints.length / width); i++) {
            for (int j = 0; j < width; j++) {
                int sprite = ints[(i * width) + j] - 1;
                if (sprite >= 0) {
                    Rect src = new Rect((sprite % 8) * spriteSize,
                            (int) Math.floor(sprite / 8) * spriteSize,
                            (sprite % 8) * spriteSize + spriteSize,
                            (int) Math.floor(sprite / 8) * spriteSize + spriteSize);
                    Rect dst = new Rect(j * square, i * square, (j + 1) * square, (i + 1) * square);
                    canvas.drawBitmap(bitmap, src, dst, null);
                }
            }
        }
    }

    public void renderBottom(Canvas canvas) {
        canvas.drawBitmap(buffer, 0, 0, null);
    }

    public void renderTop(Canvas canvas) {
        canvas.drawBitmap(roofBuffer, 0, 0, null);
    }

}

The code has comments for pretty much everything, we basically take 3 (more in the future, probably) arrays of ids that we generated with Tiled and draw them on 2 canvases (of the size of the whole map, usually bigger than the device screen), one to be drawn before our hero (and NPCs, in the future) which contains the ground elements and the objects where our hero won't be able to walk through (note, this isn't implemented yet), and the other with elements that might cover our hero, or objects. The main idea is that it looks exactly like it does on Tiled, with our hero character moving around it.

Now, let's see changes in other classes necessary for this to work.

GamePanel.java
public class GamePanel extends SurfaceView implements SurfaceHolder.Callback {
    private GameMap gameMap;

    public GamePanel(Context context) {
        super(context);
        // Adding callback (this) to surface holder to catch events
        getHolder().addCallback(this);

        // Create our hero and load it's bitmap
        hero = new Hero(BitmapFactory.decodeResource(getResources(), R.drawable.lyon), 0, 448, 2, 64);

        // Create our game map, load bitmap, fill buffers
        gameMap = new GameMap(BitmapFactory.decodeResource(getResources(), R.drawable.tileset), new int[] {2,24,32,2,2,32,2,24,32,2,
                2,24,32,24,32,2,2,32,24,32,
                1,1,1,1,1,2,32,2,32,2,
                8,8,16,8,16,16,1,2,2,32,
                8,16,16,8,8,16,8,16,8,8,
                31,31,31,31,31,8,8,16,16,8,
                5,6,6,7,31,31,31,31,31,31,
                13,14,14,15,31,31,31,34,35,35,
                13,14,14,15,31,31,31,44,29,30,
                21,22,22,23,31,31,31,44,37,38}, new int[] {39,40,0,0,0,0,0,0,0,0,
                0,0,0,39,40,0,0,71,0,0,
                0,0,0,0,68,0,0,0,65,66,
                55,0,0,0,0,0,0,0,0,0,
                0,0,0,72,0,0,0,0,0,0,
                0,0,0,0,0,0,0,72,0,0,
                0,0,0,0,0,0,0,0,0,48,
                0,0,0,0,0,0,0,0,0,0,
                0,49,0,0,63,64,0,0,0,0,
                0,0,0,0,0,0,0,0,0,0}, new int[] {0,0,0,0,0,0,0,0,0,0,
                0,0,0,59,60,61,0,0,57,58,
                47,0,0,67,0,69,0,0,0,0,
                0,0,0,0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,0,59,60,
                0,33,0,0,0,0,0,0,67,0,
                0,41,0,0,57,58,0,0,0,0,
                0,0,0,0,0,0,0,0,0,0,
                0,0,0,0,0,0,0,0,0,0}, 10, 64);

        // Create the Game Loop (the thread)
        thread = new MainThread(getHolder(), this);

        // Make GamePanel able to focus so it can handle events
        setFocusable(true);
    }

    public void render(Canvas canvas) {
        // Fill screen with black
        canvas.drawColor(Color.BLACK);
        gameMap.renderBottom(canvas);
        // Tell our hero to draw itself
        hero.draw(canvas);
        gameMap.renderTop(canvas);
    }

    public void update() {
        //Check if hero reached right wall
        if (hero.getX() + 64 >= getWidth()) {
            hero.setX(0);
        }
        // Tell our hero to update itself
        hero.update();
    }
}

Note: this is showing ONLY the changed/added variables/methods, not the whole code, update accordingly.

We added a GamePanel variable to the class and initialise it on our constructor (there are 3 int[] arrays, those are from our generated .txt from Tiled, as shown in the picture tutorial above, mines are probably different than yours, use the ones you get! They're, in order, terrain, object and roof).

The Hero initialising method now has one more int in it (with value 64), as it'll be required for the changes made to the class.

We call both GameMap's render methods on our render(Canvas canvas), the first one before drawing our hero, the other after, as previously discussed. There's a minor change to our update() method, just to move the character from left to right, got tired of watching it moving from top to bottom :).

Finally, some changes in our Hero class to complete this post.

Hero.java
public class Hero {

    public Hero(Bitmap bitmap, int x, int y, int direction, int square) {
        this.bitmap = bitmap;
        this.x = x;
        this.y = y;
        this.direction = direction;
        this.sprite = 0;
        this.spriteHeight = bitmap.getHeight() / 4; // 4 directions
        this.spriteWidth = bitmap.getWidth() / 3;   // 3 sprites per direction
        this.src = new Rect(0, direction * spriteHeight, spriteWidth, (direction + 1) * spriteHeight);
        this.dst = new Rect(x, y, x + square, y + square);
        Log.d(TAG, "Hero created!");
    }

    public void update() {
        x += 5;
        dst.left = x;
        dst.right = x + spriteWidth;

        sprite++;
        src.left += spriteWidth;
        src.right += spriteWidth;
        if (sprite > 2) {
            sprite = 0;
            src.left = 0;
            src.right = spriteWidth;
        }
    }

}

Note: this is showing ONLY the changed/added variables/methods, not the whole code, update accordingly.

As we mentioned, the constructor takes a new int parameter, this is to change the size of the sprite rendered on screen, which will allow our game to do things like zoom in/out, in the future. The only other change is on the update() method, this is to move our hero from left to right.

That's it! Run it now and you'll see our hero moving left to right on the map you designed :P

Hope you guys enjoyed it! Share this post and follow me on social networks! :) I appreciate it!!!

Take care!!!

No comments: