Converting TDMC to use Tiles
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.
Introducing: tile_meeting()
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.
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.
tile_meeting_precise()
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:
if(!instance_exists(obj_tile_wall)) instance_create_depth(0,0,0,obj_tile_wall);
Next we go down into the area of the code that is looping through the tile locations. Specifically this part:
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.
if(_tile){
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; if(place_meeting(argument0, argument1,obj_tile_wall)) return true;
And that's it!
The full script looks like this:
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.
Conclusion
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!
Get TDMC 2.0 - for GameMaker 2022+
TDMC 2.0 - for GameMaker 2022+
Simple & Versatile top-down movement and collision for GameMaker
Status | Released |
Category | Assets |
Author | Pixelated Pope |
Tags | collision, GameMaker, gms2, scripts, sourcecode, Top-Down |
More posts
- Version 3.0.1 - Bug FixJul 22, 2022
Comments
Log in with itch.io to leave a comment.
Sorry if you already answered these questions, but did you give the solution for a speed of 1.25 and using a for loop in the tile_meeting function, doesn't that affect performance too much?
I was hoping that the issue would sort of "auto resolve" with the new floating point collision detection. Though I haven't tested recently with those changes. I had a solution in the past, but it was very complicated, and it's much easier to set your speed to 1.251 or something.
And as a rule, we don't worry about performance until there is a tangible issue. So as far as I know, there is nothing to be concerned about performance wise.
I haven't tried this yet, but for everyone trying fractional speeds, maybe this code made by ShaunJs can help dealing with this, since you're still moving only in integer intervals
Fractional Collisions / Pixel Perfect - Pastebin.com
Turns out it actually works
var _v = keyboard_check(ord("S")) - keyboard_check(ord("W"));
var _h = keyboard_check(ord("D")) - keyboard_check(ord("A"));
direction = point_direction(0, 0, _h, _v);
if ((_v != 0) or (_h != 0)) {
spd = lerp(spd, spd_max, 0.2);
} else {
spd = 0;
}
spd_final = spd + spd_f;
spd_f = spd_final - floor(abs(spd_final))*sign(spd_final);
spd_final -= spd_f;
movement_and_collision(direction, spd_final, "Walls");
btw, its a fantastic script the one you made PixelatedPope
So, just FYI, the current beta of GMS2 dramatically changes how collision checks are done. They are no longer whole number based!!!! This is actually massively huge. So fractional speeds no longer really matter anymore. If your bbox_left is .00001 pixels away from a wall, you aren't colliding.
What this also means is that "snapping" to a wall with the classic x += sign(hspd) code can be further refined with an "accuracy" multiplier x+= sign(hspd)*.01
Will it slow you down a bit? Yeah, maybe, but it will make all these floating point errors and jitters go away; possibly for every collision system ever. Very exciting.
I didn't know this until now, it actually sounds really cool. That for sure will solve a lot of issues. Can't wait for it to be fully released. Thanks for the response
yo the reflection in your demo draws over the terrain elements which kinda breaks the illusion.
10/10 asset tho
Yeah, I saw that... and I thought "I suppose I could set up the reflection as a sprite asset on another layer, or a duplicate object at a higher depth...." and then I got lazy :D
i understand this on a spiritual level
heyo, i know its very simple but im not getting it for some reason. Are you saying that the main hero object would have this
script_execute(tile_meeting,x,y,"pathing_blockers");
in it's step event? "pathing_blockers" would be the name of the layer with collision walls. I'm sure im way off because im getting an error.
No no.
You need to use that function to control your collision checking.
In most tutorials for movement, you would do something like this:
if(place_meeting(x+hspd, y, objWall){
//Collide
} else {
x+=hspd
}
tile_meeting REPLACES place_meeting in this instance. So if you are following a tutorial that uses object collision and you want to use tile collision, just replace place_meeting with tile_meeting.
if(tile_meeting(x+hspd, y, "pathing_blockers"){
//Collide
} else {
x+=hspd
}
Don't use script_execute(). There's almost never any reason to use that anymore.
My gosh was i barking up the wrong tree. Thank you so much.
Don't feel bad. This is a really important distinction that many tutorials don't cover: the line between "Collision Checking" and "Collision Handling" is often blurred. TDMC is a collision "handling" system. It utilizes place_meeting as it's "collision checking" function, but all the logic AROUND the calls to place_meeting are what you are really paying for when you purchase TDMC. Collision checking is pretty simple: "Is there something there or not?", while collision handling is really complicated: "Okay, there is something there... what do I do about it?"
Once you understand that these are two separate pieces to the puzzle, it makes more sense that you can simply replace "place_meeting" in any tutorial with a custom script, and get similar behavior with more control over what is a "blocker" and what isn't.
Hello! Thanks for another amazing tutorial! I have a question that feels like should have a simple answer but it is not coming to me...
When declaring the initial _x _y variables, you include "(argument0 - x)" and similar. However, argument0 *is* x, so "(argument0 - x)" will always equal 0, right? Of course, this assumes the instance calling the script is the one that a collision check is being run for.
However, if a second instance calls this script in order to run a collision check on a different instance, then "x" and "argument0" in the script refer to different x values... Wouldn't that break the script? I just can't think of a use case where you would want this? Am I missing something?
Edit: I may have figured it out, it's to allow for a collision checks outside of the bounding box? Like when you want to determine if a moving object will collide given its current velocity?
Exactly. It's pretty rare that you are checking a collision at your exact position, so the argument passed in is usually your position + an offset. But more importantly is that we are combining this with bbox_ variables. Since bbox is also a variable that is always "relative" to the x position of the calling object, combining it with the offset gives us final range of tiles that needs to be checked for collision.
Cheers! I think what confused me was the conversion of the script into the 2.3+ format. I replaced argument0 and argument1 in the function declaration with x and y (as that's what you had in the @param comments) and that changed the x and y already in the script to be script variables instead of instance variables (and thus, would always result in zero for the aforementioned calculations). All good now!
I had an issue where as soon as my player object collided with a tile, the object would instantly stick and would not move with further input. I was able to resolve this by putting "self." in front of the x and y values in the scripts. Note, I did not modify the variables that started with an _, only the single character variables.
I did this for both the "tile_meeting_precise" script and the "movement_and_collision" script.
Works like a charm! Thank you for the asset!
That is... a bit terrifying. Putting "self" in front of anything shouldn't do anything. Ever (outside of the context of a constructor, anyway).
Whoops, sorry! I didn't see a notification for your message previously. I've since moved on to a different collision system for my current project as I couldn't get the collision just right for my needs (my project isn't a top down game, but I was trying to get the pixel perfect collision working for my platformer style game).
If I remember correctly, GMS2 was seeing x and y as a variable in the script, rather than the x and y instance variable of the object. When putting "self." in front of it, the script found the calling object's instance variables and it worked as expected. Maybe an update of GMS2 caused this?
Maybe? That's why we usually declare vars as _x or _y instead of just x and y unless we literally want to deal with the calling instance's position. Still feels unlikely, but as long as you got moving forward it really isn't important.
I'm having this exact issue. This works perfect with the test square and it really is pixel perfect. However it prevents my player object from moving at all.
*editing because jump still works, but im sticking into walls and on floors. Last night I played with the player object mask and got it working with a detailed tileset, but switched to a simpler 32x16 block tileset today and this started happening.
Just so I understand, the collision handling code you had previously that used place_meeting worked fine, but when you swapped to tile_meeting it started behaving badly?
This will be a difficult issue to resolve in this thread. I would recommend joining the Game Maker Community Discord Server and asking there. Most members are familiar with my system and could help you trouble shoot, and if I'm available, I'll help personally. Here's an invite.
https://discord.gg/gamemaker
I had this issue too, I only had to put self. in these sections of both the the tile_meeting and tile_meeting_precise scripts:
https://imgur.com/GdzjVHe
I also had to adjust the script some to include the whole wrapper for some reason, like this (with the end brace down at bottom ofcourse):
https://imgur.com/O89ztcH
I had to adjust the spr_wall_tile_frames origin to be Top-Left also.
I had some problems at first, so I post them here, so maybe this helps someone: I had my origin for "spr_wall_tiles_frames" set to middle middle. This needs to be set to upper left of course. Before I found this issue I made the sprite visible which helped debugging. I also set the FPS to 0, then you can actually see very well what is happening.
Pixelated Pope
I changed TDMC code in my game and the example project to use precise tile collision and noticed that if you move diagonally against tile wall, the player kind of studders, which looks odd.
This does not happen with the object based collision. Have you noticed this? You wouldn't happen to have any idea how to fix that?
It is noticeable at sub pixel speeds, 1.25 for example
Yup 1.25 is sort of the worst speed imaginable for my system.
I DO have a solution... but oh boy, is it weird.
I probably need to write another blog post on the solution, but I don't know when I'll have time for that.
In the mean time, try picking a move speed that doesn't stutter too bad, or just "deal with it" until I can get the post up.
The fix should slot in pretty easily to any existing tile based collision system, so you shouldn't have to worry about continuing with your project and finishing it later.
thank you for the quick reply!
Good to hear that you have something in the works already ;) - can't wait.
Yup. It's a rare nitpick, but one I was prepared for... it's just not an easy to consume fix so it isn't something I was willing to include in the asset for people who probably wouldn't even want/need it.
Are the created "obj_tile_wall" instanced cleaned up again somewhere? (I assume that on larger levels the number of created instances could still become substantial?)
I can't seem to find code for this in the "tile_meeting_precise" script.
To be sure I've incorporated an alarm[0] in the "obj_tile_wall" object itself, which destroys it after a number of frames.
I look forward to your thoughts!
Sorry, it seems you simply repurpose the one "obj_tile_wall" instance, repositioning it as necessary.
Please do comment if I've misunderstood this.
Yup, there is only ever one instance. It just gets moved and re-used for every collision check. It gets destroyed on room change and recreated as necessary. No need to concern yourself with cleaning it up.
Manually typed out the first script and decided to post here so others don't have to
///@descrition tile_meeting(x,y,layer)
///@param x
///@param y
///@param layer
var _layer = argument[2],
_tm = layer_tilemap_get_id(_layer);
if (_tm == -1 || layer_get_element_type(_tm) != layerelementtype_tilemap) {
show_debug_message("Checking collision for non existent layer / tilemap")
return false;
}
var _x1 = tilemap_get_cell_x_at_pixel(_tm, bbox_left + (argument[0]-x),y ),
_y1 = tilemap_get_cell_y_at_pixel(_tm, x,bbox_top + (argument[1]-y) ),
_x2 = tilemap_get_cell_x_at_pixel(_tm, bbox_right + (argument[0] - x),y ),
_y2 = tilemap_get_cell_y_at_pixel(_tm, x, bbox_bottom + (argument[1]-y) );
for(var _x = _x1; _x <= _x2; _x++){
for(var _y = _y1; _y <= _y2; _y++) {
if (tilemap_get(_tm,_x,_y)) {
return true;
}
}
}
return false;
Thanks but if you click the image it will take you to PasteBin to get the source code as well.
ty so much. PasteBin hasnt loaded for me at all.