GML Odd Numbered Mask Dimensions and Tile Collisions

Hi all,

Context:
Getting back into a game maker project after over a year and was playing around with some code and functionality I have posted below (not sure what happened to my old forum account but had to register again even though I have on here for at least 5 years now?). The code seemingly works fine when the sprite size is even numbered (i.e., 30x30 etc.) and smaller than the tile size and if I need to go bigger I can add more vertices to check. The game is top down and the walls I am colliding with are randomly generated tiles. The issues only seems to happen when going left AND when sprite size is odd. I am using the automatic ellipse collision mask. The size of the sprite in the gif example is 29x29 and the tile size references in the code as CELL_HEIGHT and CELL_WIDTH are both 32. I have added code that allows the player character to slide around corner walls, but when fully commenting that code out the issue is still present. You will notice I have a combination of tile and object based collisions, this is on purpose as I am combining the buttery smooth movement code for awkwardly shaped objects and a tile collision system for proc generated levels.

Issue:
When moving left AND colliding with a wall of tiles where no opening exists on the bottom or top left corners, the player shoots down and snaps to lower sections of the grid. This will force the player through any tiles if there are any below it and pop out on the other side. If an opening exists on the top or bottom the code to slide slowly up or down around the corner executes as intended.

Is this an issue with the bit-wise operators being used in the tile collision and can I resolve this? Is this a limitation of calculating tile collisions using this method and should I just ensure all my mask indices are even numbered? If you need any other context please let me know and I can update the post.

I appreciate any and all information shared!

Example gif of the problem.

Code:
GML:
/// @description Tile Collisions

//@WARNING these collisions apparently do not work for sprite sizes that are in odd numbers?
//If the collider is larger than the tile size then you need to check more points than just the corners.
//////////////////////////////////////////////////////////////////////////////
//////////////TILE COLLISIONS////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////

var width_slide_distance = bbox_right - bbox_left ; //pixels to check for colliding with corners to slide around them
var height_slide_distance = bbox_bottom - bbox_top;


y += ySpeed;


//Moving Up or Down
#region Tile Collisions on the Y-Axis

//Moving Down
if (ySpeed > 0) {
    //get tile map info
    var bot_left = tilemap_get_at_pixel(tilemap, bbox_left, bbox_bottom) & tile_index_mask;
    var bot_right = tilemap_get_at_pixel(tilemap, bbox_right, bbox_bottom) & tile_index_mask;
   
    //Get info for sliding when moving down
    var bot_right_slide_left = tilemap_get_at_pixel(tilemap, bbox_right - height_slide_distance, bbox_bottom) & tile_index_mask;
    var bot_left_slide_right = tilemap_get_at_pixel(tilemap, bbox_left + height_slide_distance, bbox_bottom) & tile_index_mask;

    //if both bottom left and right corners are in contact with tiles, stop
    if (bot_left != 0 and bot_right != 0) {
        y = ((bbox_bottom & ~(CELL_HEIGHT-1)) - 1) - spriteBBBottom;
        ySpeed = 0;
    }
    else {
        //tile on bottom left, but not right so try to slide right
        if (bot_left != 0 and bot_right == 0) {
            //check if there is a tile to the right of the bottom left corner
            if (bot_left_slide_right == 0) {
                if (!input_left) && (!place_meeting(x+2, y, oParWall)) {
                    x += 2;
                }
                y = ((bbox_bottom & ~(CELL_HEIGHT-1)) - 1) - spriteBBBottom;
                ySpeed = 0;
            }
            else {
                 y = ((bbox_bottom & ~(CELL_HEIGHT-1)) - 1) - spriteBBBottom;
                ySpeed = 0;
            }
        }
        if (bot_left == 0 and bot_right != 0){
            if (bot_right_slide_left == 0) {
                if (!input_right) && (!place_meeting(x-2, y, oParWall)){
                    x -= 2;
                }
                y = ((bbox_bottom & ~(CELL_HEIGHT-1)) - 1) - spriteBBBottom;
                ySpeed = 0;
            }
            else {
                y = ((bbox_bottom & ~(CELL_HEIGHT-1)) - 1) - spriteBBBottom;
                ySpeed = 0;
            }
        }
    }
}
//Moving Up
else {
    var top_left = tilemap_get_at_pixel(tilemap, bbox_left, bbox_top) & tile_index_mask;
    var top_right = tilemap_get_at_pixel(tilemap, bbox_right, bbox_top) & tile_index_mask;
   
    //Get info for sliding up
    var top_right_slide_left = tilemap_get_at_pixel(tilemap, bbox_right - height_slide_distance, bbox_top) & tile_index_mask;
    var top_left_slide_right = tilemap_get_at_pixel(tilemap, bbox_left + height_slide_distance, bbox_top) & tile_index_mask;
   
    if (top_left != 0 and top_right != 0) {
        y = ((bbox_top + CELL_HEIGHT) & ~(CELL_HEIGHT-1)) - spriteBBTop;
        ySpeed = 0;
    }
    else {
        if (top_left != 0 and top_right == 0) { //tile on bottom left, try to slide right
            if (top_left_slide_right == 0) { // if there is no tile a slide distance away to the right
                if (!input_left) && (!place_meeting(x+2, y, oParWall)) {
                    x += 2;
                }
                y = ((bbox_top + CELL_HEIGHT) & ~(CELL_HEIGHT-1)) - spriteBBTop;
                ySpeed = 0;
            }
            else {
                y = ((bbox_top + CELL_HEIGHT) & ~(CELL_HEIGHT-1)) - spriteBBTop;
                ySpeed = 0;
            }
        }
        if (top_left == 0 and top_right != 0){
            if (top_right_slide_left == 0) {
                if (!input_right) && (!place_meeting(x-2, y, oParWall)) {
                    x -= 2;
                }
                y = ((bbox_top + CELL_HEIGHT) & ~(CELL_HEIGHT-1)) - spriteBBTop;
                ySpeed = 0;
            }
            else {
                y = ((bbox_top + CELL_HEIGHT) & ~(CELL_HEIGHT-1)) - spriteBBTop;
                ySpeed = 0;
            }
        }
    }
}
#endregion



x += xSpeed;


//COMMIT TO CHANGES IN X COORDINATES
//apply xsp value to x coordinate


#region Tile Collisions on the X-Axis

//Moving Right
if (xSpeed > 0) {
   
    //return some tile map info
    var top_right = tilemap_get_at_pixel(tilemap, bbox_right, bbox_top) & tile_index_mask;      
    var bot_right = tilemap_get_at_pixel(tilemap, bbox_right, bbox_bottom) & tile_index_mask;
   
    //Slide checking
    var top_right_slide_down = tilemap_get_at_pixel(tilemap, bbox_right, bbox_top + width_slide_distance) & tile_index_mask; //tile 5 pixels below?
    var bot_right_slide_up = tilemap_get_at_pixel(tilemap, bbox_right, bbox_bottom - width_slide_distance) & tile_index_mask;
   
    //tile on both corners, then stop
    if (top_right != 0 and bot_right != 0) {
        x = ((bbox_right & ~(CELL_WIDTH-1)) - 1) - spriteBBRight;
        xSpeed = 0;
    }
    else {
        if (top_right != 0 and bot_right == 0){ //tile at top right, but not bottom right, so check below top right corner
            if (top_right_slide_down == 0) { // no tile present below top right then slide
                if (!input_up) && (!place_meeting(x, y+2, oParWall)) {
                    y += 2;
                }
                x = ((bbox_right & ~(CELL_WIDTH-1)) - 1) - spriteBBRight;
                xSpeed = 0;
            }
            else {
                x = ((bbox_right & ~(CELL_WIDTH-1)) - 1) - spriteBBRight;
                xSpeed = 0;
            }
        }
        if (top_right == 0 and bot_right !=0 ) {
            if (bot_right_slide_up == 0){
                if (!input_down) && (!place_meeting(x, y-2, oParWall)) {
                    y -= 2;
                }
                x = ((bbox_right & ~(CELL_WIDTH-1)) - 1) - spriteBBRight;
                xSpeed = 0;
            }
            else {      
                x = ((bbox_right & ~(CELL_WIDTH-1)) - 1) - spriteBBRight;
                xSpeed = 0;
            }
        }
    }
//Moving Left
} else {
    var top_left = tilemap_get_at_pixel(tilemap, bbox_left, bbox_top) & tile_index_mask;
    var bot_left = tilemap_get_at_pixel(tilemap, bbox_left, bbox_bottom) & tile_index_mask;
   
    //Slide checking
    var top_left_slide_down = tilemap_get_at_pixel(tilemap, bbox_left, bbox_top + width_slide_distance) & tile_index_mask; //tile 5 pixels above?
    var bot_left_slide_up = tilemap_get_at_pixel(tilemap, bbox_left, bbox_bottom - width_slide_distance) & tile_index_mask;

    //if tile present on both corners, then stop
    if (top_left != 0 and bot_left != 0) {
        x = ((bbox_left + CELL_WIDTH) & ~(CELL_WIDTH-1)) - spriteBBLeft;
        xSpeed = 0;
    }
    else {
        if (top_left != 0 and bot_left == 0){ //tile at top left, but not bottom left, so check below top left corner
            if (top_left_slide_down == 0) { // no tile present below top left corner then slide
                if (!input_up) && (!place_meeting(x, y+2, oParWall)) {
                    y += 2;
                }
                x = ((bbox_left + CELL_WIDTH) & ~(CELL_WIDTH-1)) - spriteBBLeft;
                xSpeed = 0;

            }
            else {
                x = ((bbox_left + CELL_WIDTH) & ~(CELL_WIDTH-1)) - spriteBBLeft;
                xSpeed = 0;
            }
        }
        if (top_left == 0 and bot_left !=0 ) { // tile present at the bottom left but not the top left
            if (bot_left_slide_up == 0){
                if (!input_down) && (!place_meeting(x, y-2, oParWall)) {
                    y -= 2;
                }
                x = ((bbox_left + CELL_WIDTH) & ~(CELL_WIDTH-1)) - spriteBBLeft;
                xSpeed = 0;
            }
            else {      
                x = ((bbox_left + CELL_WIDTH) & ~(CELL_WIDTH-1)) - spriteBBLeft;
                xSpeed = 0;
            }
        }
    }
}
#endregion
 

Roldy

Member
First I'd suggest to think about your movement speed and bounding box sizes and how it relates to the fact that functions like place_meeting will floor the x and y inputs. So if you have non integer speeds, none integer position or non integer bbox sizes then you might get results that behave different than the actual input you give.

Second I would just put some show_debug_messages in there and see how many times this is getting called. When you slide down the wall it moves faster than your movement speed, but over several frames. So it is likely you are not pushed far enough out to stop the collision and you keep triggering every frame.

The suspect line is anywhere you have y+=2.

On as side note I was shocked that these lines even compile:

GML:
if (!input_up) && (!place_meeting(x, y+2, oParWall)) {
...
}
But they do and seem to work perfectly fine.
 
Hi Roldy, thanks for the response. I also thought it had something to do with the y += 2, but that portion of the code is functioning as intended and here is an example gif of what it does. Basically it checks the top and bottom corners of the player (if moving left or right) or the left and right corners (if moving up and down) and slides the object slowly over if there is no tile touching one of the corners to smoothly go around corners. The reason I don't believe that is the issue is because I can comment out all of that functionality and the issue persists.

I am going to do what you suggested and set up some debug lines to see what's going on here as it is quite strange that it only has issues with odd numbered sprite sizes, which would in turn affect the mask size. I think you are onto something with the number rounding issues. I just made the width an even number (30) and height odd (29) and there is no issue in any direction. When you switch it however, the issue is back.

The line you quoted there is for when the object is sliding around a corner, I wanted to make sure you continue sliding unless there is a wall object as I am mixing tile and object collisions. I shouldn't be altering the y value directly like that though so I can see why it looks fishy. Here is an example gif of what I mean. I'd love to know why you don't think the line would compile 😅, looking at it now it doesn't look good and needs another pair of parenthesis I guess, but it keeps on compiling (haven't tried the yoyo compiler though).

Back to the problem, I honestly don't why it would be number rounding but not affect the other directions of movements, but that seems like the next best thing to investigate. I am hoping someone can confirm whether it is or is not an issue with the bit wise operators because they are the most foreign to me.
 

TheouAegis

Member
On as side note I was shocked that these lines even compile:

GML:
if (!input_up) && (!place_meeting(x, y+2, oParWall)) {
...
}
But they do and seem to work perfectly fine.
Why wouldn't they?


@JealousCrow As for odd vs even, where is the sprite origin? If the origin is centered, then it is closer to the top-left of the bounding box than any other sides.
 
Hi TheouAegis, the origin is centered. Is there anyway to have a sub pixel origin? I calculate these variables in the create event of the object with the below code which is used in the step and end step events (sorry for the lack of definition in the original post), but I don't think this will solve the issue will it. Given the pixel grid starts at 0,0 for a sprite, wouldn't odd numbers give you the perfect center point? Is this logic correct?

GML:
//Get sprite bbox info
spriteBBLeft    = sprite_get_bbox_left(sprite_index) - sprite_get_xoffset(sprite_index);
spriteBBRight    = sprite_get_bbox_right(sprite_index) - sprite_get_xoffset(sprite_index);
spriteBBBottom    = sprite_get_bbox_bottom(sprite_index) - sprite_get_yoffset(sprite_index);
spriteBBTop        = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
I feel like the simple solution here is keep the dimensions of sprites even numbers, but I don't want to be working with an impractical system.
 

Yal

🐧 *penguin noises*
GMC Elder
not sure what happened to my old forum account but had to register again even though I have on here for at least 5 years now?)
Yoyo is only allowed to retain your personal information for 1 year after you use their services (due to the GDPR) so your account will be deactivated if you're inactive for more than 365 days. You might be able to have your account re-activated again if you contact support, but I'm not sure.
 

TheouAegis

Member
Hi TheouAegis, the origin is centered. Is there anyway to have a sub pixel origin? I calculate these variables in the create event of the object with the below code which is used in the step and end step events (sorry for the lack of definition in the original post), but I don't think this will solve the issue will it. Given the pixel grid starts at 0,0 for a sprite, wouldn't odd numbers give you the perfect center point? Is this logic correct?

GML:
//Get sprite bbox info
spriteBBLeft    = sprite_get_bbox_left(sprite_index) - sprite_get_xoffset(sprite_index);
spriteBBRight    = sprite_get_bbox_right(sprite_index) - sprite_get_xoffset(sprite_index);
spriteBBBottom    = sprite_get_bbox_bottom(sprite_index) - sprite_get_yoffset(sprite_index);
spriteBBTop        = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
I feel like the simple solution here is keep the dimensions of sprites even numbers, but I don't want to be working with an impractical system.
Origins are points, not pixels. A point has no dimensions, a pixel has two. You can't have a subpixel origin. What you could try to do with that code you have there is subtract 0.5 from each of those variables. I doubt it will help you, but then again it's possible with the rest of your code that could be the only change you need.
 
Top