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:
***
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: