Megatiler 3 - Tile Movement (Part 1)

Posted March 1, 2021

NOTE:
This tutorial is most likely not compatible with versions of SGDK above 1.70. Unfortunately, I simply do not have the capacity or ability to update them right now. Read more about it here. Sorry.

Hold up!

If you like 90s platformers, check out my new game Kid Bubblegum!

We’ve got our world and we’ve got our player. A world is boring if you can’t explore, so let’s use tile movement to get our player going!

We’ll handle most of our player’s movement in a single function, which we’ll call movePlayer. Create an empty one with the following signature for now:

void movePlayer(moveDirection Direction){
    //Move the player
}

As you can see we’re passing in an argument of type moveDirection, which, as you’ll remember, is an enum we defined containing the directions up, down, left, right and none. This will tell our player which direction to move in.

But in order to move the player we need to grab input from the joypad. So, as usual, we’ll implement a callback function that does just that:

void myJoyHandler(u16 joy, u16 changed, u16 state)
{
    if (joy == JOY_1)
    {
        if (state & BUTTON_UP)
        {
            movePlayer(up);
        }
        else if (state & BUTTON_DOWN)
        {
            movePlayer(down);
        }
        else if (state & BUTTON_LEFT)
        {
            movePlayer(left);
        }
        else if (state & BUTTON_RIGHT)
        {
            movePlayer(right);
        }
    }
}

Looks simple enough! Of course this won’t do anything yet, but one step at a time. Before we actually get to the movement code, tell SGDK to use the callback at the beginning of main():

JOY_init();
JOY_setEventHandler(&myJoyHandler);

Moving the Player

A bit of theory before we begin. As our world is tile-based, we will make player movement tile-based as well. And I mean that in the strictest sense: The player will only be able to move from tile to tile, and only one tile at a time. As that is the most basic form of this kind of movement, it’s a good introduction to the overall concept and implementation.

Basically, how it works is like this: When we press a direction, the game checks whether the corresponding tile next to the character is empty. If so, the character starts moving towards that tile and stops when it reaches it. If the tile isn’t empty however, the character simply doesn’t move at all. No fancy collision detection here, just looking up numbers in a simple array!

But enough with the theory, let’s get coding to get a better sense of how it all works. Of course our movePlayer function still needs to do… well, anything at all, really. Let’s start adding the function body to get our player character in gear!

First off, we’ll add an if-statement that will wrap the entire function body.

void movePlayer(moveDirection Direction){
    if (player.moving == FALSE)
    {
        //Do movement things
    }

Our player entity contains the value moving which we will toggle whenever its moving or not moving. If the character is already on the move we’ll leave it alone until it has reached its destination; otherwise we might move it out of the tile grid, which would kind of break the game. So everything in movePlayer will be wrapped in this condition.

Next, we’ll have to check which direction the player is supposed to move in. We do this with a big switch statement that checks the value of the Direction parameter.

switch( Direction ){
    case up:
        break;
    case down:
        break;
    case left:
        break;
    case right:
        break;
    default:
        break;

We’ll now fill in each of the conditions in turn, starting with up. What do we actually want to do, though? Well, we said we’d check whether the tile the character wants to move to is actually empty, so let’s code that! But first, let’s make a small helper function that’ll come in handy throughout the entire project.

int getTileAt(u8 X, u8 Y)
{
    return *(&level1[0][0] + (Y * MAP_HEIGHT + X));
}

As the name implies, this function returns the tile value at a specific coordinate of our map. It starts at the address of the first item in the array, then moves down the rows and columns to the value we want. It’s a quick and handy way to check values in our level map.

Note: As Andi correctly pointed out in the comments below, this function could also simply return &level1[y][x]. However, having the function work as we defined it will be useful later on. How’s that for ominous foreshadowing?

Oh, and let’s make things a bit easier still, by adding another define at the top of the file:

#define SOLID_TILE 1

We used the value 1 to represent a solid wall tile in our level array. It’s a good idea to give these values names, so it’s easier to keep track of them.

But now back to movePlayer!

Since we have our helper function now, let’s use it to see if the tile above the player is empty using this if-statement:

if(player.tilePos.y > 0 && getTileAt(player.tilePos.x,player.tilePos.y-1) != SOLID_TILE){

}

As you’ll remember, the tilePos struct inside the player entity stores the x- and y-coordinate of the player in tiles. So, the first thing we check is whether the player is already at the top edge of our map, by checking if tilePos.y > 0. Without this check we could simply leave the map and the edge of the screen, which isn’t good.

The second condition grabs the tile above the player and checks if it is supposed to be solid. That’s pretty much it, really! It’s rather simple once you get the logic behind it. If there is a tile above the player, and if that tile is empty, the player can move up.

But what should happen if the condition is met? This:

player.tilePos.y -= 1;
player.moving = TRUE;
player.dir = Direction;

We update the tile-coordinate of the player entity, tell the game that it’s moving, and set the direction (to up in this case).

Now, this won’t actually move the player on the screen. Remember that tilePos is simply a struct we created; it doesn’t have any bearing on the movement of the actual sprite on the screen. We’ll take care of that soon, but first, let’s take care of the other directions. For reference, here is what the entire up block should look like:

switch( Direction ){
    case up:
    {
        if(player.tilePos.y > 0 && getTileAt(player.tilePos.x,player.tilePos.y-1) != SOLID_TILE){
            player.tilePos.y -= 1;
            player.moving = TRUE;
            player.dir = Direction;
        }
        break;
    }
    case down:
    //...

The down case is going to look very similar:

if(player.tilePos.y < MAP_HEIGHT - 1 && getTileAt(player.tilePos.x,player.tilePos.y+1) != SOLID_TILE){
    player.tilePos.y += 1;
    player.moving = TRUE;
    player.dir = Direction;
}

We’re checking if the player is above the bottom edge of the map, then check the tile below. If the coast is clear, tilePos.y is updated. Remember that MAP_HEIGHT stores the total height of our map (8) but that our map is an array with indexes 0–7. That’s why we have to check for MAP_HEIGHT - 1.

I encourage you to try and add the code for the left and right cases for yourself. But here are the juicy bits, just to make sure:

case left:
    if(player.tilePos.x > 0 && getTileAt(player.tilePos.x-1, player.tilePos.y) != SOLID_TILE){
        player.tilePos.x -= 1;
        //...
    }
    break;
case right:
    if(player.tilePos.x < MAP_WIDTH - 1 && getTileAt(player.tilePos.x+1, player.tilePos.y) != SOLID_TILE){
        player.tilePos.x += 1;
        //...
    }

And that gives us the framework for moving our player around the grid. So far the movement is only theoretical; we can update the tile position of the player, but nothing is actually moving yet. We’ll take care of that in the next part!

If you've got problems or questions, join the official SGDK Discord! It's full of people a lot smarter and skilled than me. Of course you're also welcome to just hang out and have fun!

Join my Discord Server!

Hang out, get news, be excellent!

Come hang out!

Want To Buy Me a Coffee?

Coffee rules, and it keeps me going! I'll take beer too, though.

Check out the rest of this tutorial series!

  • Megatiler 1 - Tiles
  • Megatiler 2 - Spawning the Player
  • Megatiler 3 - Tile Movement (Part 1)
  • Megatiler 4 - Tile Movement (Part 2)
  • Megatiler 5 - Coins
  • Megatiler 6 - HUD, Sound and Making an Exit
  • Megatiler 7 - Loading Levels
  • By using the Disqus service you confirm that you have read and agreed to the privacy policy.

    comments powered by Disqus