GMS 2.3+ Trouble getting my head around tile-based Ceiling-Slope-Collision

Barney

Member
Hello everyone!
I have tried for a couple of weeks now to get my platformer collisions to work properly in GMS2.
I have followed both Mike Dailly's and Shaun Spaldings tutorial on the matter but they only ever create the floor Collision for sloped tiles,
I need the ceiling tiles to work much the same way, since my player interacts with them relatively frequently.

It is so important, in fact, that I have contemplated switching back to Unity's engine, even though I run into other problems there. (Notably the worst possible way to handle frame based animation known to civilization)
I could get it to work with my Raycasting Engine there.

So what's my exact problem then...?
I tried to follow what has been said in both Tutorials, the common ground there, being that we take a screenshot of the collision tile map and fill an array with the amount of pixels on the y-axis for each pixel on the x-axis.
So I quickly figured out, we need to reverse the pixel-counting logic to get the same number of pixels for the ceiling tiles. (Counting from the bottom upwards).
I got that to work quite nicely as I checked my arrays in the console.
I then foolishly thought I could re-use much of the code that handles the bottom slope tiles, to no avail.

My problem here is that I apparently don't know where to place the character based on the collision that has happened and spent my last 2 weeks trying to figure this out.
I calculated with example collisions to figure out where my math went wrong and got rid of the bouncing, however once I reversed the gravity back to normal (pointing downwards, had it upwards to test the ceiling)
I get stuck the slopes once I collide with them.
I move nicely along them... I just stop falling down, even though Gravity is accelerating me constantly, which I also checked for.

I hope anyone can give me at least a couple pointers to this problem, as the floor tiles work very nicely now.
However that is not enough in a 2D Metroidvania, as you might imagine.
 

TheouAegis

Member
What's your tile detection and upward collision code? Are you colliding with one point or with two points? It should be the same as downward collision, but with different points on the sprite and use the ceiling height map. When moving horizontally, though, you need to use the floor map for the bottom collision point and the ceiling map for the top collision point.
 

Barney

Member
Oh thanks for your reply.
Jeah I use 2 points on the bbox bottom and bbox top respectively, as well as one in the top middle/ bottom middle which is the one colliding with the slopes.

I use a modulo based system that detects how far in the tile the player is and sets him back out of bounds +-1 pixel

Honestly, I'm willing to rewrite my entire code if you (or anyone else) got any better solutions, though I really like the idea of tile collisions, so much easier to design rooms on the go with it and test gameplay.
 

Barney

Member
It very well might be, but I was so lost in this one, after days, I started trying out different things, just to see what works... (In the following example, TILE_SIZE is just 16)
So here for example is my Vertical collision:

Code:
//Vertical Collision
//Check to see if the Tile is a Slope, and don't detect slopes (Tilemapindex 2 and above) with this system.
if(tilemap_get_at_pixel(tilemap, x, bbox_bottom + y_speed) <= 1 || tilemap_get_at_pixel(tilemap, x, bbox_top + y_speed) <= 1)
{

    if(y_speed > 0) {bbox_side = bbox_bottom;}
    else {bbox_side = bbox_top;}
    p1 = tilemap_get_at_pixel(tilemap, bbox_left, bbox_side + y_speed);
    p2 = tilemap_get_at_pixel(tilemap, bbox_right, bbox_side + y_speed);
    if((p1 == 1) || (p2 == 1))
    {
        if(y_speed > 0) {y = y - (bbox_bottom mod TILE_SIZE) + (TILE_SIZE - 1);}
        else {y = y + bbox_top mod TILE_SIZE;}
        y_speed = 0;
    }
}

y += y_speed;
You can see, I experimented with the "else part" down below, due to frustration of not knowing what's up.
Code:
y = y + bbox_top mod TILE_SIZE;
Should not be working. Yet the original solution let the character bounce constantly for me , while this one doesn't.
Also, here is the "Collision Tilemap" that get's referenced every couple lines here.

f90a0c76-fc99-4419-848a-efc068c353d5.png

As you can see, just 16 by 16 px Tiles in one Line starting from empty to full to up slope and down slope 2 tiles. Then comes the ceilling.
For now I only bothered with these.
 

Barney

Member
So let's start with the Slopes system that is currently in place.
It starts by dividing the tile collision sprite (spr_tile_collision) as seen in the post above into it's pixel count on x and 16 pixel chunks, giving each tile an ID
This is pretty much 1 to 1 from the Tutorial and appears to work just fine.
Code:
//Tile init variables and macros:
#macro TILE_SIZE    16

heightstoget = sprite_get_width(spr_tile_collision);
tiles = heightstoget / TILE_SIZE;

//Create Tiles
var layerid = layer_create(0, "Tiles");
tilemapid = layer_tilemap_create(layerid, 0, 0, ts_collision, tiles, 1);

for(var i = 0; i <= tiles; i++)
{
    tilemap_set(tilemapid, i, i, 0);
}
So here is the init objects draw event, filling the Arrays:

Code:
draw_tilemap(tilemapid, 0, 0);
for(var i = heightstoget - 1; i >= 0; i--)
{
    var check = 0;
    while(check <= TILE_SIZE)
    {
        global.heights[i] = check;
        if(check == TILE_SIZE) {break;}
        if(surface_getpixel(application_surface, i, check) != c_black) {break;}
        check++;
    }

    
    var check = 0;
    while(check <= TILE_SIZE)
    {
        global.peaks[i] = (TILE_SIZE - check);
        if(check == TILE_SIZE) {break;}
        if(surface_getpixel(application_surface, i, check) == c_black) {break;}
        check++;
    }
    
}
//show_debug_message(global.heights);
//show_debug_message(global.peaks);
room_goto_next();
Here we could have the first potential room for error, as I might have messed up the array filling up part for "global.peaks"
Here is what the debug says about those two:
Global Heights =
Code:
[16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

15,15,14,14,13,13,12,12,11,11,10,10,9,9,8,8,7,7,6,6,5,5,4,4,3,3,2,2,1,1,0,0,

0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ]
So you can see the ceiling slopes get counted as 0, which is a full block because the program tries to count until it finds something that is not empty space but immediately finds the bottom of the ceiling slope.
So global.peaks should do the same but in reverse:
Code:
[ 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,

0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,

16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,0,0,

0,0,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,

15,15,14,14,13,13,12,12,11,11,10,10,9,9,8,8,7,7,6,6,5,5,4,4,3,3,2,2,1,1,0,0,

0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15 ]
With my code, however, it doesn't.
It counts the first 2 tiles the same way and then the bottom slopes as empty tiles (apart from the filled out part, which gets counted as 0 = full 16 pixel space)
but then counts the ceiling slopes correctly again.
I didn't bother with anything but the slopes as the code in the post before this should catch any solid and empty tile anyways and it only ever collides with ceiling slopes up there.
 

Barney

Member
Up next we have the actual script that gets called in the step event, after both horizontal and vertical collisions are done.

The functions use the spr_tile_collision, the current x position and the y position after y_speed, just for reference, the actual code where this gets called will be posted below, during the next post.

This is the part that is for the floor tiles, again making a safety check to see if it's a solid tile.
Code:
function InFloor(tilemap, _x, _y)
{
    var pos = tilemap_get_at_pixel(tilemap, _x, _y);

    /*Check if the Tile is at least a solid tile,
    because Tile 0 is always an empty tile in GMS2*/
    if(pos > 0)
    {
        //If the position is a solid block tile, returns how deep we are in the floor
        if(pos == 1) {return (_y mod TILE_SIZE);}
        var thefloor = global.heights[(_x mod TILE_SIZE) + (pos * TILE_SIZE)];
        return ((_y mod TILE_SIZE) - thefloor);
    }
    else {return -(TILE_SIZE - (_y mod TILE_SIZE));}
}
And here is my very hacky code, where I tried to calculate through with some set cases to see if the math is any sound. I am not a big math guy anymore, so I had to do this.
Code:
function InCeiling(tilemap, _x, _y)
{
    var pos = tilemap_get_at_pixel(tilemap, _x, _y);
    show_debug_message("Pos = " + string(pos));

    if(pos > 0)
    {
        if(pos == 1) {return -(_y mod TILE_SIZE);}
        //x = 182, y = 29, pos = 6
        //(x mod tile) = 6, pos * tile = 6 * 16 = 96
        //global.peaks[102] = 12
        var theceiling = global.peaks[(_x mod TILE_SIZE) + (pos * TILE_SIZE)];
        //(y mod tile) = 13
        //returns 25
        return ((_y mod TILE_SIZE) + theceiling);
    }
    else {return -(TILE_SIZE - (_y mod TILE_SIZE));}
}
I kinda don't understand why Shaun sort of overwrote his other collision in this instance.
the
Code:
if(pos == 1) {return (_y mod TILE_SIZE);}
code happens for the 16 by 16 full block of the tile_collision sprite.
Similarly, I don't fully understand the "else" part at the end, handling the empty tile at pos 0 in the sprite.
 

Barney

Member
And finally, the call to this script in the player step event.

Again, first I post the original verison, the one that works and then my ceiling slope adaptation, that sorta works, but doesn't...
Code:
//Check how deep in slope, move accordingly

var floordist = InFloor(tilemap, x, bbox_bottom + y_speed);

if(floordist >= 0)
{
    y += y_speed;
    y = y - (floordist + 1);
    y_speed = 0;
    floordist = -1;
}
Note: floordist is set to -1, because that's what it would be after the calculation has finished, I guess this takes out having to recalculate this the next frame.

Just for completions sake, here is how that handles walking down slopes, as well.
Code:
//Walk Down Slopes
if(grounded)
{
    y += abs(floordist)-1;
    //if at base of current tile
    if((bbox_bottom mod TILE_SIZE) == TILE_SIZE - 1)
    {
        //if the slope continues
        if(tilemap_get_at_pixel(tilemap, x, bbox_bottom + 1) > 1)
        {
            //move there
            y += abs(InFloor(tilemap,x,bbox_bottom + 1));
        }
    }
}
Works like a charm.


Now here is my code after weeks of hacking and whacking about:
Code:
//Check how deep in ceiling slopes, move accordingly
var ceildist = InCeiling(tilemap, x, bbox_top + y_speed);

if(ceildist >= 0)
{
    show_debug_message("Distance to Ceiling is " + string(ceildist));
    show_debug_message("X Coord is: " + string(x));
    show_debug_message("Y Coord is: " + string(y));
    show_debug_message(y_speed);
    y += y_speed;
    //y = 29 -(25-16) = 20
    y = y - (ceildist - TILE_SIZE);
    y_speed = 0;
    ceildist = -1;
}
As you might notice, I left my comment in here, which is following my previous comments on the example case calculation, this returns 20 , which was the exact spot in my room that was 1 pixel below the slope, as it should be.

And all of this code results in working slopes when gravity is negative BUT I get stuck at the ceiling at the moment.
Not permanently mind you but it won't properly return my gravity from the initial jumping impulse that I have right now.
 

Barney

Member
I feel a bit disappointed, absolutely no one can help me with this.
It's like it's outside of the engines capability to include this stuff in a sensible manner to begin with.
I can see why people stopped bothering about this Engine 3 Years ago.
 

TheouAegis

Member
I kinda don't understand why Shaun sort of overwrote his other collision in this instance.
the
This is because snapping a pixel to the grid moves that pixel up. When moving down an encountering a collision, what way do you want to move the pixel? Up. So a simple snap is fine. When moving up and colliding, you want the pixel to move down, so snapping isn't enough.


else {return -(TILE_SIZE - (_y mod TILE_SIZE));}
I see no reason for this line, because your Step event says to only use the value of floordist if it is greater than 0, and in this case floordist will always be less than 0 if pos is 0.


if(ceildist >= 0)
Putting it all together, ceildist is probably always less than 0.


I'm trying to process in my head how the code would actually work with ceiling slopes, but my cat will not shut the $@&# up.
 

Barney

Member
else {return -(TILE_SIZE - (_y mod TILE_SIZE));}
In the Tutorial he said this would be to "just be consistent with what we are returning here" and I don't know why but changing this does change my movement behavior, I haven't tried removing it yet.
if(ceildist >= 0)
Sadly that's not as simple, it was my initial guess as well, changing it to if(ceildist <= 0) results in me moving down indefinitely (even though I turned my gravity upwards) so it constantly sees the air and thinks it's colliding, trying to put me down below the "tile" until I reach the floof of my room, where it can't go further due to the other collision code.

Also, thank you, I did not know that snapping pixels results in them snapping upwards.
I wonder if I can give you the project file somehow (it's not very large) if you think that helps, going through the issue, I think seeing it is better than reading about it...
 

TheouAegis

Member
If it's GMS2, I can't view it. They extended my license, but it's expired now. These days I just write my own GMS1 equivalents when I need to test something.
 

Barney

Member
I am still unsure whether it's a small math issue or the entire code just doesn't play out this way.
I think rewriting the entire thing might be the way to go so if you have any input on that, I'd be more than appreciating that one.
Like come on... Eventually someone has to figure this out...
 
Top