How to Implement a Grid Movement Mechanic for Your Tile-based Game

“Hey, you! You’re sliding all over the place! STOP! “, you project your thoughts violently at the character sliding around your screen, getting caught on the corner of tiles, other objects, your pride. You stand up out of your computer chair with a sigh, dreaming about the grid movement mechanics in old-school Pokemon and Zelda games. If that’s the kind of thing you want for your game, then you’ve come to the right place.

Let’s get started. We’ll roll our own basic Tile-engine agnostic grid movement mechanic.

Click here for a TL;DR

The idea

This is what we’re trying to emulate:

Grid-based Tile Movement Mechanic
Character movement clamped to the tile grid (16×16) tiles.

For this to work, we need to maintain an invariant in our implementation. An “invariant” is something that never changes. This is a guarantee.

Our invariant will be that the player’s movement will always start and end on a tile location. That is, our player’s position will be exactly the position of a tile coordinate. Not halfway on, but entirely.

In the animated gif above, my tiles are 16×16 pixels each. The tile size is our movement constraint, we can only move one tile at a time (16 pixels) in a given direction.

Since we’re working with a 2d game, we only have two coordinate axes: x,y. Each tile has an (x,y) coordinate and so do the characters. In fact, assuming we have a regular coordinate system (where +y is up, and +x is to the right), for something to move up we need to increase its y value, for something to move down we must decrease it.

Vector Coordinates
The 2D coordinate system.

We can represent directions as a coordinate pair, too. If the player is going up, their direction coordinates will be (0, 1). If the player is moving left, it will be (-1, 0). It’s just an indicator for which direction they’re going.

We’ll call this the direction vector. We’ll be using 2d vectors a lot in this example. You don’t need to have an in-depth understanding of what a vector is for this tutorial, but I highly suggest you look into them when you have a chance.

The implementation

For this basic implementation, we need to uphold our invariant that we discussed earlier. This means that the character must spawn or is positioned exactly on a tile location. Since my tiles are 16×16, any multiple of 16 will do, like say, 7 tiles over * 16 pixels = 112.

We need to keep track of the character’s current position, target position, and direction. We’ll store these as 2d vectors.

private Vector2 direction;
private Vector2 position;
private Vector2 targetPosition;

Our initial position and direction should be set when the character is created.

If our targetPosition isn’t null, then we have an active target position and thus are considered to be moving.

private boolean isMoving() {
    return targetPosition != null;
}

When the character receives a move order, we should ignore it if we’re already moving. Otherwise, we can update our direction (and corresponding animation if applicable!) and calculate which tile we’re moving to based on our direction.

protected void move(Vector2 direction) {
    if (isMoving()) {
        return;
    }
 
    if (direction.equals(Vector2Utils.Up)) {
        currentAnimation = walkUp;
    }
    // ... removed other animation direction updates
 
    this.direction.set(direction);
    targetPosition = calculateNewTargetPosition(position);
}

To calculate the target position tile:

private Vector2 calculateNewTargetPosition(Vector2 currentTarget) {
    Vector2 newTarget = new Vector2(direction);
    newTarget.scl(TILE_SIZE);
    newTarget.add(currentTarget);
    return newTarget;
}

We’ll take our current direction and scale it by the size of our tile. Basically we’re saying, move TILE_SIZE pixels in this direction. We will produce the tile location that we’re moving to by adding our current position to this vector.

For example, if we are moving down then:

direction = (0, -1)
newTarget = (0, -1)
currentTarget = position = (112, 64)

Then we scale it by the size of our tile (16 pixels):

newTarget = (0, -16)

And add it to our currentTarget:

currentTarget + newTarget = (112 + 0, 64 + (-16)) = (112, 48)

What if the player lets go of the movement key before we’ve reached our target position? How do we mark ourselves as not moving so that we can stop? Like so:

We check if we’re still moving in our update(float dt) method that can be called in our main game loop.

@Override
public void update(float dt) {
    // other character updates omitted.
 
    if (isMoving()) {
        finishMove(dt);
    }
}

To continue to move, we’ll update the position by creating a new Vector which is initially set to our direction, then scaled by how fast we want to move. This is just a plain ol’ basic movement update.

Then, we’ll check if we’ve arrived at our target position. Remember, since we’re using floats (or at least I am), we need to account for precision in determining if we’re in the right spot. If we just compared straight across without remembering about float precision, we wouldn’t stop moving because 112.0 != 112.00001. Because our movement is based on the delta time, it’s important that we use an epsilon value large enough such that we don’t skip over our target by accident.

So, we’ll compare with an epsilon to see if we’re within 111.0-113.0, basically, plus or minus 1 pixel from our target position.

If we’ve arrived, we’ll clamp the position to the target position, and set the targetPosition to null to indicate that we’re not moving, and are thus moveable.

private void finishMove(float dt) {
    if (!isMoving()) {
        return;
    }
 
    position.add(new Vector2(direction).scl(dt * DEFAULT_SPEED));
    if (position.epsilonEquals(targetPosition, 1f)) {
        position.set(targetPosition);
        targetPosition = null;
    }
}

This set up also makes it easy for us to adding collision checking; but that’s a topic for a future post.

TL;DR

A character’s movement’s start and end point must always be completely on a valid tile location (aka, the location is a multiple of your tile size).

When movement is detected, the character is in one of two states:

  1. in a valid tile position (moveable)
  2. moving from one tile to the next (moving)

If the character is moveable, then we can calculate the next tile over in the character’s current direction (or the new direction if the character changes direction/player clicks different move key). This is the target position. The character is now considered moving.

If the character is moving, then it will ignore any new movement requests until it has reached the target position that was calculated when it received the movement request.


Don’t hesitate to jump into the comments or reach out to me on Twitter with your thoughts or alternative ways to implement something like this! I’d love to hear them.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s