Collision is an important concept for video games. As players move through your world, when a collision is done right, it makes your game feel more realistic. They’re given a pseudo-6th sense of touch, and they instinctively understand the boundaries and limitations of the world. However, when collision detection isn’t done correctly, your game’s world will fail to make that instinctive connection. In addition, gameplay can often be severely jepordized and/or ruined. In this tutorial you’ll learn how to implement a grid-based tilemap collision detection in your Game Boy games.
Like all other tutorials in my How to Make a Game Boy game tutorial series, we’ll use GBDK 2020 and C Programming. Be sure you’ve read these 3 tutorials as well:
- Getting Started With 2020 GBDK
- Drawing on the Background and Window Layers
- Drawing and Moving Sprites in Gameboy Games
The basic math used in this tutorial can be applied to other game engines as well.
Grid-Based Math for Tilemap Collision
Custom collision detection always requires a little math. Don’t worry, you won’t need to solve for x. Grid-Based Math is actually extremely simple. We just need some basic division, addition, and subtraction.
We’ll use basic math to determine:
- Given an x and y coordinate, what column and row (respectively) the pixel is on.
- How to get that tile’s index in a tilemap array
Calculating Column & Row
Given a set of x & y coordinates, we can determine which row and column these coordinates lie on very simply. All we do is divide the x & y coordinates by the size of a single grid node. The x coordinate becomes the column, and the y coordinate becomes the row.
- If your grid is 8×8, divide them both by 8.
- If your grid is 16×16, divide them both by 16.
- If you have an abnormally sized grid that is 8×16, divide the x coordinate by 8, and the y coordinate by 16.
Codewise:
uint8_t column = worldX / GRID_SIZE;
uint8_t row = worldY / GRID_SIZE;
GBDK doesn’t support floating-point numbers. Handling them is very simple. After division, simply round any of the previous quotients down to the nearest integer. That’s it!
Here’s a visual of converting various points to rows and columns:
Finding a Tile in a tilemap array
If our tilemap array is 2-dimensional, we can directly plug in the row & column values we previously calculated. However, if your tilemap is 1-dimensional, a tiny-bit more math is needed.
Additionally, you’ll need an extra piece of information: the width of your tilemap (in tiles). This is not to be confused with the length of the tilemap array.
Here’s the equation to determine, given a column and row, a tile’s index in it’s associated tilemap array.
Here’s an equivalent in C code:
uint8_t column = worldX / GRID_SIZE;
uint8_t row = worldY / GRID_SIZE;
uint16_t tileIndex = column + (row*TILEMAP_WIDTH);
You can visualize all of that like so:
NOTE: In GBDK, if your grid-size is a power of 2 (commonly 8). It’s more performant to use bit-shifting instead of division. Simply bit-shift by the power of 2, instead of the grid size.
- Division by 2? Bit shift to the right once.
- Division by 4? Bit shift to the right twice.
- Division by 8? Bit shift to the right three times.
If our grid size is 8px, we can shift the bits of our x and y variables to the right three times. This essentially is a division by 8.
uint8_t column = worldX >> 3; // Division by 8
uint8_t row = worldY >> 3; // Division by 8
uint16_t tileIndex = column + (row*TILEMAP_WIDTH);
Just for clarity. If we wanted to determine row and column from tile index, we could do the reverse using these equations:
Which is the following in C code:
// If we already have a tile index, and want to determine row & column
uint8_t column = tileIndex % TILEMAP_WIDTH;
uint8_t row = tileIndex / TILEMAP_WIDTH;
Be careful of array/level boundaries
Always make sure your row, column, and tile indices are within the proper ranges.
- Make sure none of them are negative.
- Make sure the column isn’t larger than your tilemap’s width (in tiles).
- Make sure your row isn’t larger than your tilemap’s height (in tiles).
- Make sure your index is less than the length of the tilemap array.
How you handle coordinates that don’t meet these conditions is up to you. I commonly consider them as solid tiles, which can help prevent the player from going out of bounds.
Tilemap Collision in GBDK Games
In its most simple form, for GBDK games, you can do the following:
- Check where the player wants to move
- What tilemap tiles will they collide with?
- If any of those tiles are solid, we stop the player from moving there.
- Otherwise, we move the player to the desired location.
From a high level:
// If the player is moving at all, We'll handle horizontall and vertical movement separately
// Handling them separately enables a sliding motion against walls
if(playerIsMoving){
// If we are moving horizontally
if(directionX!=0){
// Check the tile on the next location
// If none are solid
if(!WorldPositionIsSolid(newPlayerX,playerY)){
// Update the player's x position
playerX=newPlayerX;
}
}
// If we are moving vertically
if(directionY!=0){
// Check the tile on the next location
// If none are solid
if(!WorldPositionIsSolid(playerX,newPlayerY)){
// Update the player's y position
playerY=newPlayerY;
}
}
}
NOTE: We check the new X and Y coordinates separately so the player can slide along walls.
But the above tilemap collision example only works if our player is a single point. If our character has a square or rectangular bounding box, we’ll need to check multiple different points when moving.
NOTE: How many points you’ll need to check depends on the size of your object and the size of your collision grid. For example, if our object is 16×16 and our collision grid nodes are 8×8, we’ll need to check a middle point to make sure no accidental overlap occurs. If our object was 24×24, we’d have to check two middle points.
In the above code snippet, we used a custom function called “WorldPositionIsSolid”. Here’s what that function looks like:
uint8_t WorldPositionIsSolid(uint16_t x, uint16_t y){
// Bit-shifting would be faster here
uint16_t column = x/GRID_NODE_SIZE;
// Make sure the tile is in proper bounds
if(column>=TILEMAP_WIDTH_IN_TILES)return TRUE;
uint16_t row = y/GRID_NODE_SIZE;
// Make sure the tile is in proper bounds
if(row>=TILEMAP_HEIGHT_IN_TILES)return TRUE;
uint16_t tilemapIndex = column+row*TILEMAP_WIDTH_IN_TILES;
uint8_t tileIsSolid = FALSE;
// Get the tilset tile in our tilemap
uint8_t tilesetTile = tilemap_map[tilemapIndex];
// In our tileset, the solid tiles always come first.
// There are 10 tiles. The first 9 are solid
// this makes it fast & easy to determine if a tile is solid or not
tileIsSolid = tilesetTile<NUMBER_OF_SOLID_TILES;
return tileIsSolid;
}
If our player is 16×16.We need to check 3 different points on each axis during movement. This prevents accidental overlap.
If the player is moving horizontally:
- We check the top corner on the side their moving
- We check the middle of the edge on their side their moving
- We check the bottom corner on the side they are moving
If the player is moving vertically:
- We check the left corner on the side their moving
- We check the middle of the edge on the side their moving
- We check the right corner of the side they are moving.
// If the player is moving at all
// We'll handle horizontall and vertical movement separately
// Handling them separately enables a sliding motion against walls
if(playerIsMoving){
// If we are moving horizontally
if(directionX!=0){
// Check the tiles on our side
uint8_t solid =
WorldPositionIsSolid(newPlayerX+directionX*PLAYER_HALF_WIDTH,playerY-PLAYER_HALF_HEIGHT)||
WorldPositionIsSolid(newPlayerX+directionX*PLAYER_HALF_WIDTH,playerY)||
WorldPositionIsSolid(newPlayerX+directionX*PLAYER_HALF_WIDTH,playerY+PLAYER_HALF_HEIGHT);
// If none are solid
if(!solid){
// Update the player's x position
playerX=newPlayerX;
}
}
// If we are moving vertically
if(directionY!=0){
// Check the tiles above or below us
uint8_t solid =
WorldPositionIsSolid(playerX+PLAYER_HALF_WIDTH,newPlayerY+directionY*PLAYER_HALF_HEIGHT)||
WorldPositionIsSolid(playerX,newPlayerY+directionY*PLAYER_HALF_HEIGHT)||
WorldPositionIsSolid(playerX-PLAYER_HALF_WIDTH,newPlayerY+directionY*PLAYER_HALF_HEIGHT);
// If none are solid
if(!solid){
// Update the player's y position
playerY=newPlayerY;
}
}
}
That covers the basics of tilemap collision. You can find the full source code on github here. I’ll mention two good tutorials you might like next: The Smooth Motion via Scaled Integers tutorial, and the RPG-Style 4-directional Movement tutorial.