• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

SOLVED Moving At Non-Integer Values With Tile-Based Collision System?

yarrow

Member
EDIT: Solved my problem and wrote a tutorial explaining the solution. The TLDR is that I needed to store the fractional amount of my movement speed in a container, rather than simply re-adding the fractional amount to movement speed at the end of the collision check (or at the beginning of the next collision check). Once the container reaches a whole number, the extra pixel gets added to the move speed for that step's collision check, then the extra pixel is subtracted from move speed after movement. It's a little more complicated but the tutorial explains everything.

***

EDIT: Not actually solved but I'm going to reformulate my question in a new post to better clarify the issue/my confusion

***

Hi everyone,

I recently started making a low-resolution 2D platformer and have been storing/re-adding fractional portions of my horizontal speed at the beginning of my tile-based collision script in order to check collisions at whole pixel values but save the decimals to be added back later.

This weekend, after implementing a more nuanced movement system with acceleration/deceleration and minimum speeds at values less than one pixel, I realized that my storing/flooring decimals system is currently incompatible with the kind of subpixel movement I'm hoping to use, as it only allows movement at whole integers or greater (i.e. pixels).

With the horizontal storing/flooring code in place, my player object hesitates for half a second before movement (since it's waiting for a whole-pixel increment to move). When I comment out the horizontal storing/flooring code, the movement is PERFECT but the collisions are totally broken, of course. Hmm.

Do I need to rethink where I store/re-add fractions during the collision check, or are tile-based collision systems simply incompatible with subpixel movement speeds? I've been racking my brain and am thinking I may need to opt for object-based collisions instead, but am new to programming and wanted to see if anyone else has run into this issue.

Any thoughts or advice you may have would be greatly appreciated! And please let me know if any further information would be helpful for diagnostic purposes.

Here is my collision script:

GML:
function collisionCheck(){
    // Reset Decimal Count
    if (hSpeed == 0) hSpeedDecimal = 0;
    if (vSpeed == 0) vSpeedDecimal = 0;

    // Apply Carried-Over Decimals
    hSpeed += hSpeedDecimal;
    vSpeed += vSpeedDecimal;

    // Floor Speed & Store Decimals
    hSpeedDecimal = hSpeed - (floor(abs(hSpeed)) * sign(hSpeed));
    hSpeed -= hSpeedDecimal;
    vSpeedDecimal = vSpeed - (floor(abs(vSpeed)) * sign(vSpeed));
    vSpeed -= vSpeedDecimal;

    // Horizontal Collision
    var side;
    var maskHeightHalf = (bbox_bottom + bbox_top) * 0.5;
    var bboxBottomOffset = (sprite_height - 1) - sprite_get_bbox_bottom(sprite_index);

    // Determine Which Side To Test
    if (hSpeed > 0) side = bbox_right else side = bbox_left;
    var xOriginOffset = side - x;

    // Test Top & Bottom (& Middle) Of Side
    var t1 = tilemap_get_at_pixel(global.tilemap, side + hSpeed, bbox_top);
    var t2 = tilemap_get_at_pixel(global.tilemap, side + hSpeed, bbox_bottom);
    var t3 = tilemap_get_at_pixel(global.tilemap, side + hSpeed, maskHeightHalf);
    var t4 = tilemap_get_at_pixel(global.tilemap, side + hSpeed, bbox_bottom + bboxBottomOffset + 1);

    // Collision Found
    if    ((t1 != VOID) and (t1 != SEMISOLID) and t1 != UND) or
        ((t2 != VOID) and (t2 != SEMISOLID) and t2 != UND) or
        ((t3 != VOID) and (t3 != SEMISOLID) and t3 != UND) or
        ((t1 == UND) and (t2 == UND) and (t3 == UND) and (t4 == SOLID)) {
        if hSpeed > 0 {
            // Heading Right; Move Flush Against Collision Tile's bbox_left
            var t1x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_top);
            var t2x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_bottom);
            var t3x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, maskHeightHalf);
            var t4x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_bottom + bboxBottomOffset + 1);
            // Snap To Smallest (Leftmost) x Position Times Tile Size Minus 1
            var tileBboxLeft = minPos(t1x, t2x, t3x, t4x) * TILE_SIZE;
            x = tileBboxLeft - 1 - xOriginOffset;
        } else {
            // Heading Left; Move Flush Against Collision Tile's bbox_right
            var t1x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_top);
            var t2x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_bottom);
            var t3x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, maskHeightHalf);
            var t4x = tilemap_get_cell_x_at_pixel(global.tilemap, side + hSpeed, bbox_bottom + bboxBottomOffset + 1);
            // Snap To Largest (Rightmost) x Position Plus 1 Times Tile Size
            var tileBboxRight = (max(t1x, t2x, t3x, t4x) + 1) * TILE_SIZE;
            x = tileBboxRight - xOriginOffset;
        }
        hSpeed = 0;
    }

    x += hSpeed;
    // Clamp Horizontal Movement To Room Dimensions
    if bbox_left <= 0 {
      x = (x - bbox_left);
      hSpeed = 0;
    } else if bbox_right >= room_width {
      x = room_width - 1 - (bbox_right - x);
      hSpeed = 0;
    }

    // Vertical Collision
    var side;
    var maskWidthHalf = (bbox_right + bbox_left) * 0.5;

    // Determine Which Side To Test
    if (vSpeed > 0) side = bbox_bottom else side = bbox_top;
    var yOriginOffset = side - y;

    // Check Left & Right Side (& Middle)
    var t1 = tilemap_get_at_pixel(global.tilemap, bbox_left, side + vSpeed);
    var t2 = tilemap_get_at_pixel(global.tilemap, bbox_right, side + vSpeed);
    var t3 = tilemap_get_at_pixel(global.tilemap, maskWidthHalf, side + vSpeed);
    var t4 = tilemap_get_at_pixel(global.tilemap, bbox_left, bbox_bottom);
    var t5 = tilemap_get_at_pixel(global.tilemap, bbox_right, bbox_bottom);
    var t6 = tilemap_get_at_pixel(global.tilemap, maskWidthHalf, bbox_bottom);

    // Collision Found
    if    ((((t1 != VOID) and (vSpeed > 0 or t1 != SEMISOLID)) and (t4 != SEMISOLID) or
           (t1 == SOLID and t4 == SEMISOLID)) and (t1 != UND and t4 != UND)) or
        ((((t2 != VOID) and (vSpeed > 0 or t2 != SEMISOLID)) and (t5 != SEMISOLID) or
           (t2 == SOLID and t5 == SEMISOLID)) and (t2 != UND and t5 != UND)) or
        ((((t3 != VOID) and (vSpeed > 0 or t3 != SEMISOLID)) and (t6 != SEMISOLID) or
           (t3 == SOLID and t6 == SEMISOLID)) and (t3 != UND and t6 != UND)) {
        if vSpeed > 0 {
            // Heading Down; Move Flush Against Collision Tile's bbox_top
            var t1y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_left, side + vSpeed);
            var t2y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_right, side + vSpeed);
            var t3y = tilemap_get_cell_y_at_pixel(global.tilemap, maskWidthHalf, side + vSpeed);
            // Snap To Smallest (Highest) y Position Times Tile Size Minus 1
            var tileBboxTop = (min(t1y, t2y, t3y)) * TILE_SIZE;
            y = tileBboxTop - 1 - yOriginOffset;
        } else {
            // Heading Up; Move Flush Against Collision Tile's bbox_bottom
            var t1y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_left, side + vSpeed);
            var t2y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_right, side + vSpeed);
            var t3y = tilemap_get_cell_y_at_pixel(global.tilemap, maskWidthHalf, side + vSpeed);
            // Snap To Largest (Lowest) y Position Plus 1 Times Tile Size
            var tileBboxBottom = (max(t1y, t2y, t3y) + 1) * TILE_SIZE;
            y = tileBboxBottom - yOriginOffset;
        }
        vSpeed = 0;
    } else {
        // Check For Semisolid Platform Stack
        if ((t1 == SEMISOLID and t4 == SEMISOLID) or (t2 == SEMISOLID and t5 == SEMISOLID) or
            (t3 == SEMISOLID and t6 == SEMISOLID)) and vSpeed > 0 {
            // Get Platform Tiles' y Positions
            var t1y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_left, side + vSpeed);
            var t2y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_right, side + vSpeed);
            var t3y = tilemap_get_cell_y_at_pixel(global.tilemap, maskWidthHalf, side + vSpeed)
            var t4y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_left, bbox_bottom);
            var t5y = tilemap_get_cell_y_at_pixel(global.tilemap, bbox_right, bbox_bottom);
            var t6y = tilemap_get_cell_y_at_pixel(global.tilemap, maskWidthHalf, bbox_bottom)
            // If Not Same Tile, New Semisolid Collision
            if t1y != t4y or t2y != t5y or t3y != t6y {
                // Move Flush Against Semisolid Platform Top
                var tileBboxTop = (min(t1y, t2y, t3y)) * TILE_SIZE;
                y = tileBboxTop - 1 - yOriginOffset;
                vSpeed = 0;
            }
        }
    }

    y += vSpeed;

    // Calculate Distance From bbox_bottom To Adjusted y Origin
    var spriteOriginOffset = (y - 1) - bbox_bottom;
    // Clamp y If Sprite Bottom Above Room Height
    if  (bbox_bottom + bboxBottomOffset < 0) y = -(bboxBottomOffset - spriteOriginOffset);
}
 
Last edited:
Top