Tech

Precise Tile Collisions by Pixelated Pope

001 blog post

Posted by Ross Manthorp on 16 July 2020

Hey GameMakers! Pixelated Pope here, and today I want to show you a method for building your game's collision using a blend of tile-based collisions and object-based, precise collisions for the best of both worlds! First, real quick, let’s break down just a few pros and cons of both methods.

Tile-based

Pros

  • Placing tiles using the room editor is fun and easy
  • Ultra-fast performance
  • No limit to how many walls you can place

Cons

  • Always rectangular
  • No built-in support for collision_line or collision_circle, etc.

Object-based

Pros

  • Allows precise collisions for angled or curved surfaces
  • Can use the built-in “collision” events

Cons

  • The more wall objects you have, the slower your game will get.
  • Time-consuming to build complex shapes in the room editor.

This is not a comprehensive list, but it gets the point across: tiles are fast and easy to use, but always the same rectangular shape, while objects can be any size and shape you want, but are slow and tedious to use to build rooms. That means the goal is to get as many of the good bits of both systems all together with as few as the drawbacks as possible.

Since there are many, many tutorials and guides out there on how to set up your game using object-based collision, I’m going to explain how to convert your existing object-based world collision to this new system. Regardless of whether your game is a platformer, top-down action RPG, or anything else, you should be able to follow these steps to convert your existing project.

Place your tiles

Your first step is to rip out all those instances of “obj_wall” you have in your room and replace them with tiles. You’ll likely need to create a new tileset containing all the different angles and shapes you want to support. Here is what my tileset looks like:

Pope

The layout isn’t really important beyond one thing: your top left tile is empty (as always) and the tile to the right of that is completely solid. Beyond that, neither the number, layout, nor color of the tiles matter. However, I would recommend being careful with the “empty” tiles you leave in. If you accidentally draw your collision using an “empty” (such as the ones in my bottom right corner) you may get false hits. It may be worth it to “mark” intentionally empty tiles with at least one pixel of color so you know when you’ve used one by accident in your room.

Now add a new tile layer and give it a good name like “TileCollision” and assign your tileset to the layer. Now draw in all the walls using your new tileset. Here’s the room I built:

Pope

Replace "place_meeting()"

With your room rebuilt, it’s time to convert your collision system itself. It is very likely that somewhere in your collision code, you call place_meeting() at least once. Multiple times is more likely, though. We don’t want to rebuild your entire movement and collision system, because presumably that all works; whether your game is a platformer or a top-down RPG, you’ve likely already built your movement system to feel the way you want it to feel. Fortunately, all we need to do is swap place_meeting() out for something that works the same way (returns true if there is a collision, false if there isn’t) but checking tiles instead of objects! So we are going to create a new script (or function for those of you on 2.3+) called tile_meeting()

This new script will take 3 arguments, just like place_meeting(): an x, y, and the name of your collision layer. It will then use the bounding box of the calling object to loop through each tile that the object overlaps, checking for populated tiles. Using this method, it doesn’t matter how big or small your object and tiles are, any collisions should be found.

///@description tile_meeting(x,y,layer)
///@param x
///@param y
///@param layer
var _layer = argument2,
    _tm = layer_tilemap_get_id(_layer);

var _x1 = tilemap_get_cell_x_at_pixel(_tm, bbox_left + (argument0 - x), y),
    _y1 = tilemap_get_cell_y_at_pixel(_tm, x, bbox_top + (argument1 - y)),
    _x2 = tilemap_get_cell_x_at_pixel(_tm, bbox_right + (argument0 - x), y),
    _y2 = tilemap_get_cell_y_at_pixel(_tm, x, bbox_bottom + (argument1 - y));

for(var _x = _x1; _x <= _x2; _x++){
 for(var _y = _y1; _y <= _y2; _y++){
    if(tile_get_index(tilemap_get(_tm, _x, _y))){
    return true;
    }
 }
}

return false;

For this test project, I just created an object with a simple square sprite that follows my mouse. When it finds a collision with tile_meeting(), it turns red, otherwise, it stays green. The code is simple, and looks like this:

x = mouse_x;
y = mouse_y;

image_blend = tile_meeting(x,y,"Collision") ? c_red : c_green;

And the result:

Pope

The first thing you should notice is that we are NOT precise yet, tile_meeting() simply checks for ANY tile present, and it doesn’t care what that tile looks like. To get precise tile checking, we need to take a few extra steps.

Precise Tile Checker

To get precise tile checking we are going to need a few things. First, we need a sprite that is our tileset but with each tile in its own sub-image. So let’s build that.

Duplicate your tile sprite and give it a name to distinguish it. My tiles sprite is called spr_wall_tiles so I called the copy spr_wall_frames

Open the image up in the built-in sprite editor. In the top menu select Image > Convert to Frames. You should see a window similar to your tile setup window.

Pope

Set up all the appropriate values so that all of your tiles are included at the correct size and click convert! Now you have a duplicate of your tileset split up into frames. You will need to repeat this process every time you add or remove tiles from your tileset! Though if you’ve used tiles before, you know that altering your tileset is going to do a lot more damage than make you do this again… especially if you’ve already designed multiple rooms with said tileset.

Close the sprite editor and go back to the sprite itself. We need to change a few options.

First, make sure the origin is still in the top left. Second, expand out the Collision Mask options, set the “Mode” to “Automatic” and the “Type” to “Precise Per Frame (Slow)”. And pay no mind to that slow warning, ha! Finally, while not critical, you might want to set the sprite’s speed to 0. It shouldn’t affect anything but might be handy in certain debugging situations (one of which I’ll show near the end).

With our sprite all ready to go, we need an object! But don’t worry! We only ever, and I mean EVER, need a single instance of this object in your room at any one time. And you won’t even need to remember to create it! I called mine obj_precise_tile_checker and assigned our newly created spr_wall_frames sprite as its sprite and set visible to false. That’s it! No code or any other options are necessary.

On to the final step.

tile_meeting_precise()

Let’s duplicate our tile_meeting() script and call the new copy tile_meeting_precise(). We just need to make a few changes to get it working as needed.

First, let’s change the top part of the code to create an instance of our checker object in the event that one doesn’t already exist. This means you never have to worry about dropping one in your room, or creating one at room start, etc.

///@description tile_meeting_precise(x,y,layer)
///@param x
///@param y
///@param layer
var _layer = argument2,
    _tm = layer_tilemap_get_id(_layer),
    _checker = obj_precise_tile_checker;
if(!instance_exists(_checker)) instance_create_depth(0,0,0,_checker); 

I’ve added a var to reference our checker object so that if you decide to rename your object, you won’t have to change the name in multiple places.

Next we need to modify the inside of the for loop. This code here:

for(var _x = _x1; _x <= _x2; _x++){
 for(var _y = _y1; _y <= _y2; _y++){
    if(tile_get_index(tilemap_get(_tm, _x, _y))){
        return true;
    }
 }
}

Currently, this code just checks for any tile in that cell, and if there is one, it returns true. Instead, we need to actually get the index of the tile and add a bit more logic.

var _tile = tile_get_index(tilemap_get(_tm, _x, _y));
if(_tile){
 if(_tile == 1) return true;

 _checker.x = _x * tilemap_get_tile_width(_tm);
 _checker.y = _y * tilemap_get_tile_height(_tm);
 _checker.image_index = _tile;

 if(place_meeting(argument0,argument1,_checker))
    return true;
}

We first get the index of the tile we are checking. If the index is not 0, then we check if it is 1. If it is 1, that means it is our completely solid tile, and we can return true without any more logic. However, if it is greater than 1, then we need to use our checker object.

The code then positions the checker object to line up precisely with the tile we are checking (why the origin being in the top left is important) and sets the image index to match the tile art. We then do a place_meeting with that object. If it finds a collision, we return true!

The final script looks like this:

///@description tile_meeting_precise(x,y,layer)
///@param x
///@param y
///@param layer
var _layer = argument2,
    _tm = layer_tilemap_get_id(_layer),
    _checker = obj_precise_tile_checker;
if(!instance_exists(_checker)) instance_create_depth(0,0,0,_checker); 


var _x1 = tilemap_get_cell_x_at_pixel(_tm, bbox_left + (argument0 - x), y),
    _y1 = tilemap_get_cell_y_at_pixel(_tm, x, bbox_top + (argument1 - y)),
    _x2 = tilemap_get_cell_x_at_pixel(_tm, bbox_right + (argument0 - x), y),
    _y2 = tilemap_get_cell_y_at_pixel(_tm, x, bbox_bottom + (argument1 - y));

for(var _x = _x1; _x <= _x2; _x++){
 for(var _y = _y1; _y <= _y2; _y++){
    var _tile = tile_get_index(tilemap_get(_tm, _x, _y));
    if(_tile){
     if(_tile == 1) return true;

     _checker.x = _x * tilemap_get_tile_width(_tm);
     _checker.y = _y * tilemap_get_tile_height(_tm);
     _checker.image_index = _tile;

     if(place_meeting(argument0,argument1,_checker)) return true;
    }
 }
}

return false;

I’ve updated my tester object to use tile_meeting_precise,

image_blend = tile_meeting_precise(x,y,"Collision") ? c_red : c_green;

and here is the result:

Pope

To give you a slightly better idea of what’s going on, I’m going to turn on the visibility of my tile checker object and turn off the visibility on my collision tile layer.

Pope

So you can see that as the green object moves around and looks for collisions, the single instance of our checker object is moved around, and it’s sub-image changed to be reused over and over again to check every oddly-shaped tile in your room! This will continue to work no matter how many different objects in your room are using the tile_meeting_precise script!

Hopefully, with this technique, your game plays exactly the same, but your overall performance should improve (especially in large rooms), you no longer have to worry about disabling walls outside of your view, and you can now use the tile editor and all its features to design the collisions in your room!

Thanks for reading, now go make something awesome!

Special thanks to @MimpyPython and the whole community at the GM Discord Server for help with this and many, many other things!

Example Project Link

Back to Top