A Brief History of Collisions
Object based collision systems are great if you are hand building your rooms in the room editor, and every room is relatively small in size. But there has always been a known downside to object based systems: performance when a large number of collision objects are present in the room, especially if those objects use "precise" collision checking. There are many techniques to try to improve performance, such as deactivating walls far away from the player, or combining large "chunks" of walls into a single, larger instance. But sometimes it just isn't enough to improve overall performance, or the required systems just feel too cumbersome to maintain.
When GMS2 came along and we got access to proper tile-maps, tile-based collision systems became the new hotness. They were lightning fast regardless of how large your area was or how many collidable tiles you had in your room. Additionally it was super easy to "paint" your world collision in the room editor by using all the features available for placing tiles. Of course, there was a draw back: tiles are rectangular, and there is no changing that. Adding "angled" walls or walls of any shape was difficult and often relied an a TON of complicated code or some costly operations, and almost always involved a lot of "setup" overhead that was tedious to maintain in your project.
Obviously, I hope to make that more simple for you.
The core of ANY object based collision system, not just the one found in TDMC, is place_meeting(). Because of this, the vast majority of collision system -platformer, top down, whatever... it doesn't matter- can be converted from using objects to tile by simply replacing place_meeting. So our first step when converting from object to tile is to write the replacement for place_meeting.
Click image for copy-pasteable code
Special thanks to @MimpyPython for writing most of this after showing him my own sloppy take.
tile_meeting() works exactly like you would expect place_meeting to work, but instead of telling it which object to collide with, you tell it which layer has your collision tiles drawn on it. Super simple.
Once you've added that to your project, you can replace all place_meeting calls with that. I modified movement_and_collision to accept a layer name (as a string) instead of an object, and replaced each instance of "place_meeting()" with "tile_meeting()" keeping the first two arguments the same and replacing the third argument with the layer passed in. If you do this, don't forget that I call movement_and_collision from WITHIN movement_and_collision, so that call will also have to be updated. Here's the result:
As you can see, all "populated" tiles are considered fully solid, even if the art of the tile suggests there should be an angle. But this is a good start. Next, we'll set up our assets so that we can make those angled collision tiles behave as expected.
Setting up your assets
For this precise tile solution to work, we unfortunately need to do a bit of work in the project. However, it's not a lot of work, and only has to be re-done any time you change your collision tile set.
First things first: we need a sprite and associated tileset that we can use to draw collisions to a tile layer. Mine looks like this, but the layout and available tiles can be whatever you want!
That being said there is one important thing that needs to be consistent: the first tile needs to be your "solid rectangle" tile. Everything else about the layout of your tileset sprite doesn't matter beyond that.
Okay, now we are getting to the magic part. Go to your sprite resources, right click on the sprite you just used as your collision tileset and duplicate it. Give it a unique name. My tileset is called spr_wall_tiles, and this duplicate I called spr_wall_tiles_frames. You'll see why in a minute. Open up the newly duplicated resource and then edit the sprite in the GMS2 sprite editor.
At the top of the screen in the menu bar there is an "image" option. Click it and select "Convert to Frames".
This will bring up a window very similar to your tileset window. Enter the correct Width and Height, the correct frames per row, and then the correct number of total frames. You don't NEED to include every possible sprite if you have some empty space in the bottom right corner of your tileset sprite like I do, but it won't hurt if you do.
Click Convert and Okay on the subsequent warning message.
This process needs to be repeated any time you change the layout of your collision tiles. It only takes a few seconds once you get the hang of it, though.
Back in the resource panel, expand the Collision Mask options and set the mode to Automatic if it isn't already, and the Type to "Precise Per Frame" and absolutely ignore that "slow" warning next to the option! Ha!
We need one more resource: a new object. I called mine obj_tile_wall. Uncheck the "visible" checkbox and set the sprite to our special modified version, in my case: spr_wall_tiles_frames.
And that's it! We are ready to dive back into tile_meeting() and modify it so that it utilizes this new object for precise collision checking.
You can duplicate tile_meeting or just modify it directly to make the following changes to enable "precise" collision checking in the script. I've chosen to duplicate it and rename it to "tile_meeting_precise()"
The first change is somewhere near the top. We want to make sure there is an instance of our obj_tile_wall that we just created before we start doing collision checking. We only need one, so if there isn't one, we create it. it doesn't matter where you create it or what layer/depth you create it at. We just need it to exist:
Next we go down into the area of the code that is looping through the tile locations. Specifically this part:
I have no idea why my vars aren't colored yellow...
First, we need to put tilemap_get on its own line so we can capture the result. This will save us some time in the immediate future.
var _tile = tilemap_get(_tm, _x, _y);
Then, we'll check if that is true or not. If it is false, there is no collision on that tile cell.
Inside this if check, if the value of _tile is equal to 1, then that means that the tile in this cell is our solid rectangular tile... and we don't need to do anything else; we can just return true.
if(_tile == 1) return true;
Now the fun part begins. If this cell has a collision tile, and it isn't our solid collision tile, then we can assume that it is one of our angled or otherwise non-rectangular tiles! Time to use our obj_tile_wall! We are going to position the object at the cell's location in the room, set the image index to the same value as our tile index stored in _tile, and then check for a collision with the it.
obj_tile_wall.x = _x * tilemap_get_tile_width(_tm);
obj_tile_wall.y = _y * tilemap_get_tile_height(_tm);
obj_tile_wall.image_index = _tile;
And that's it!
The full script looks like this:
Click image for copy-pastable code
The results are as you would expect: precise, smooth collisions with all the benefits of tile collisions, and very, very limited impact on performance relative to using many precise collision objects.
Hopefully you found this article helpful! Let me know in the comments below if you have any questions, find any issues, or have suggestions for how the code could be improved. Like I said, the basic system can be applied to just about any object based collision system, not necessarily only TDMC. If you have already purchased TDMC, thank you so much! If you've been holding off on it or any of my other assets... check out the Itch Summer Sale starting June 22nd ;)
Thanks for reading, now go make something awesome!