Legacy GM How to make pixel-perfect block collision with smooth movements ?

hijong park

Member


When i use vspeed, hspeed, friction to make the player move, I want to make the players to precisely colide with walls and stop.
Both player and walls have square hitboxes.

I made the players to move by using motion_add(), and add friction with friction = player.speed / 10.


and for the block collision I used this method at step event :

Code:
if !place_free(x+hspeed,y)
{move_contact_solid(direction,speed); hspeed = 0;}
if !place_free(x,y+vspeed)
{move_contact_solid(direction,speed); vspeed = 0;}
This method seems to work fine at first, but It's not perfect.

Sometimes when I move and hit the wall diagonally, The player gets stuck in the wall. also, This method only works with solid objects. If possible, I want to make it to work with non solid objects for better performance.
 

Lady Glitch

Member
You can try something like this to collide with non-solid stuff
Code:
if (place_meeting(x + xSpd, y, oWall))
{
    while(!place_meeting(x + sign(xSpd), y, oWall))
    {
        x += sign(xSpd);
    }
    xSpd = 0;
}
x += xSpd;
 
N

NeZvers

Guest
You can try something like this to collide with non-solid stuff
Code:
if (place_meeting(x + xSpd, y, oWall))
{
    while(!place_meeting(x + sign(xSpd), y, oWall))
    {
        x += sign(xSpd);
    }
    xSpd = 0;
}
x += xSpd;
Just NO! That works for those that start coding and it still leaves gaps.

If you are using GM:S2 than you can look into tile collision system, but I'm using more 1.4 so I go with bbox or ds_grid collision systems.
Not at home and can't remember if needed to do 1 pixel shift like in comments
Code:
hitbox_r  = bbox_right - x;
hitbox_l  = bbox_left - x;
hitbox_t  = bbox_top - y;
hitbox_b  = bbox_bottom - y;
Code:
var hin = keyboard_check(vk_right) - keyboard_check(vk_left);
var vin = keyboard_check(vk_down) - keyboard_check(vk_up);

if(hin!=0){
    hspd += acc * hin;
    hspd = clamp(hspd, -spd, spd);
}else{
    hspd = 0;   
}
if(vin!=0){
    vspd += acc * vin;
    vspd = clamp(vspd, -spd, spd);
}else{
    vspd = 0;   
}

var hsp = floor(hspd); //round down
if(place_meeting(x+hsp, y, oSolid)){ // check if collide
    hspd = 0;
    var block = instance_place(x+hsp, y, oSolid); // get ID from it
    if(hsp>0){ // move right
        x = block.bbox_left -hitbox_r -1;
    }
    if(hsp<0){ // move right
        x = block.bbox_right -hitbox_l +1;
    }
}
else{
    x+=hsp;  
}

var vsp = floor(vspd); //round down
if(place_meeting(x, y+vsp, oSolid)){
    vspd = 0;
    var block = instance_place(x, y+vsp, oSolid); // get ID from it
    if(vsp>0){ // move down
        y = block.bbox_top -hitbox_b -1;
    }
    if(vsp<0){ // move up
        y = block.bbox_bottom -hitbox_t +1;
    }
}
else{
    y+=vsp;  
}

To get more optimized game it's suggested to go with ds_grid collision which is similar to this bbox collision.

EDIT: forgot to put last bracket after if and add hspd/ vspd to x & y.
 
Last edited:
B

Bayesian

Guest
That works for those that start coding and it still leaves gaps.
When does this leave gaps? The only issue with it is that its not flexible when you want to add literally and thing else to the game. For basic collisions I like to use this.
 
N

NeZvers

Guest
@Bayesian it leaves gaps when you don't move rounded numbers (for example have acceleration 0.2) and so you can end up 0.8 away from wall. So after t,hat you are checking whole pixels and move by whole pixels. Meaning that if you are 3.8 pixels away from wall and hspd is 4 - after while loop system, you end up 0.8 fra om wall.
Sure you can the do same rounding I showed, but still, you are doing several unnecessary place_meeting checks that impact processing. In bbox collision system, you get ID and snap to its side without any loop.
And I'm assuming @hijong park is already using while loop collision, hence asking for a solution.

Don't get me wrong, Zack has good code, but there's a flaw as previously explained (you can't use decimal acceleration or need clever rounding) but it also require repeats and call place_meeting repeatedly (meaning it it's far from optimized).
 
Last edited:

Bentley

Member
Edit: ignore my post. I just realized my code will still move you to the wall at fractions so it won't be px perfect. Maybe snapping to the wall is the best wall to go.

You could collide with the wall, backtrack, and then move 1 pixel at a time until you are next to it.
I think this should work. Uncheck solid if it's checked.

Collision event with obj_solid (or w/e you name it)
Code:
x = xprevious;
y = yprevious;

var dir, dx, dy;
dir = point_direction(0, 0, hspeed, vspeed);
dx = lengthdir_x(1, dir);
dy = lengthdir_y(1, dir);

while (!place_meeting(x + dx, y + dy, other))
{
    x += dx;
    y += dy;
}

if ((bbox_left > other.bbox_right) || (bbox_right < other.bbox_left))
{
    hspeed = 0;
}
if ((bbox_top > other.bbox_bottom) || (bbox_bottom < other.bbox_top))
{
    vspeed = 0;
}
If you have two different blocks, like obj_floor and obj_wall, then you can skip the bottom part of the code and set vspeed to 0 when you collide with the floor and hspeed to 0 when you collide with the wall.

Also, this may not be perfect because you are moving at subpixels. A simple solution would be to round your hspeed and vspeed so you always move at integers.

hspeed = round(hspeed). // Same for vspeed
 
Last edited:

NightFrost

Member
The while-looping collision method works well and brings the instance next to a wall. The reason for this being, collision checking commands round coordinates before doing the testing. The same holds true to bbox_* values, they are rounded positions (which is why it is a bad idea to combine x + vspd and bbox_* testing in collisions, you can end with one-pixel errors when used values are not integers). The exception, I recall, are the commands that look at single pixels; it's been a while since I tested, but if memory serves they floor the coordinates instead before testing (so it is bad idea to combine bounding box collision checking with pixel collision checking, you can get mixed signals).

Edit - oops, I forgot to add one critical point to why the while-loop collision works: because draw routines also round the coordinates in the same manner. So, while the collisions leave fractions to the coordinates (instance might be 0.40000 away from a wall or 0.21425 inside it, etc), everything aligns because of rounding performed at all stages.
 
Last edited:

hijong park

Member
The while-looping collision method works well and brings the instance next to a wall. The reason for this being, collision checking commands round coordinates before doing the testing. The same holds true to bbox_* values, they are rounded positions (which is why it is a bad idea to combine x + vspd and bbox_* testing in collisions, you can end with one-pixel errors when used values are not integers). The exception, I recall, are the commands that look at single pixels; it's been a while since I tested, but if memory serves they floor the coordinates instead before testing (so it is bad idea to combine bounding box collision checking with pixel collision checking, you can get mixed signals).

Edit - oops, I forgot to add one critical point to why the while-loop collision works: because draw routines also round the coordinates in the same manner. So, while the collisions leave fractions to the coordinates (instance might be 0.40000 away from a wall or 0.21425 inside it, etc), everything aligns because of rounding performed at all stages.
Just NO! That works for those that start coding and it still leaves gaps.

If you are using GM:S2 than you can look into tile collision system, but I'm using more 1.4 so I go with bbox or ds_grid collision systems.
Not at home and can't remember if needed to do 1 pixel shift like in comments
Code:
hitbox_r  = bbox_right - x;
hitbox_l  = bbox_left - x;
hitbox_t  = bbox_top - y;
hitbox_b  = bbox_bottom - y;
Code:
var hin = keyboard_check(vk_right) - keyboard_check(vk_left);
var vin = keyboard_check(vk_down) - keyboard_check(vk_up);

if(hin!=0){
    hspd += acc * hin;
    hspd = clamp(hspd, -spd, spd);
}else{
    hspd = 0;  
}
if(vin!=0){
    vspd += acc * vin;
    vspd = clamp(vspd, -spd, spd);
}else{
    vspd = 0;  
}

var hsp = floor(hspd); //round down
if(place_meeting(x+hsp, y, oSolid)){ // check if collide
    hspd = 0;
    var block = instance_place(x+hsp, y, oSolid); // get ID from it
    if(hsp>0){ // move right
        x = block.bbox_left -hitbox_r -1;
    }
    if(hsp<0){ // move right
        x = block.bbox_right -hitbox_l +1;
    }
}
else{
    x+=hsp; 
}

var vsp = floor(vspd); //round down
if(place_meeting(x, y+vsp, oSolid)){
    vspd = 0;
    var block = instance_place(x, y+vsp, oSolid); // get ID from it
    if(vsp>0){ // move down
        y = block.bbox_top -hitbox_b -1;
    }
    if(vsp<0){ // move up
        y = block.bbox_bottom -hitbox_t +1;
    }
}
else{
    y+=vsp; 
}

To get more optimized game it's suggested to go with ds_grid collision which is similar to this bbox collision.

EDIT: forgot to put last bracket after if and add hspd/ vspd to x & y.

Thanks for the help! It works well.
 
N

NeZvers

Guest
@hijong park I just figured out a minor bug with rounding.
Code:
var hsp = floor(abs(hspd)) * sign(hspd);
Floor will round down and in negative numbers, it will give the wrong value.
 
Top