CardinalCoder64
Member
GM Version: 2022.1+
Target Platform: All
Download: N/A
Links: Collision Code (GitHub)
Summary:
This tutorial is to help anyone who is having gaps/overlaps in their collision code. The purpose of this tutorial is to correct these gaps/overlaps by providing a solution using a pixel- and subpixel-perfect collision system.
Tutorial:
NOTE: This tutorial does NOT take diagonal/slope collisions into account. See my other tutorial on basic linear slopes.
If you have followed previous tutorials on collision code, then you should be familiar with how basic collisions are commonly set up.
...then check for vertical collisions the same way.
This code is fine and is certainly a way to check for collisions. However, it is not pixel-perfect. Let me explain why.
When we are moving at whole number increments (move speed does not contain a decimal), this system should run perfectly. No gaps, no overlaps. Completely pixel-perfect. Right? Well, no. Once we add fractional/decimal movement (such as friction, acceleration, and/or gravity), things start to get messy. You may find gaps/overlaps in your game, which isn't good because it can break the player experience. For example, the image below shows a player (white square) with a move speed of 0.99 colliding with the wall (red squares) using the collision system above. As you can probably tell, there are some issues. There's a gap, an overlap, and the x and y coordinates are not whole numbers, meaning the player is not flush with the wall.
The reason for this is because if we are moving at a fractional/decimal speed and we approach a wall using this collision code, the code will check to see if we are 0.99 pixels away from the wall, and if we are, then the "while" loop will move us forward one whole pixel. We don't want to move forward 1 pixel, we want to move 0.99 pixels so that we can be flush with the wall. We can attempt to fix this by making the rate at which we inch up to the wall smaller, but it still won't be quite as precise.
So how do we fix this? Well, I have a simple solution. We can "snap" the player to the wall before we collide with it, putting the player exactly where he needs to be. So if we approach a wall from our right, we can use the left side of the wall to match the right side of the player. To do this, we need to establish a few variables first.
These variables give us the distance between the player's origin and the sides of our bounding box, which will be useful for re-aligning the player later on. For "bottom" and "right", we have to add 1 (reasons why explained in this video). If you've seen @GMWolf's video on tilemap collisions, then this should look familiar.
NOTE: If your collision mask differs from the sprite itself, change "sprite_index" to "mask_index". (Use Ctrl+F to find and replace)
Alright, so here is the code for our new collision system:
Alright, so let's break this down. First, we initialize our snap coordinates to zero. These will be used to snap the player to the wall at the end of our script. Then, we begin with our collision code. We make a "for" loop to check for a collision with a wall each pixel ahead of the player. This is necessary for overspeed correction, so that when the player is moving at speeds greater than the width of the wall, the player will still collide (thanks to @Happy_Malware for pointing out this issue). Then we do our collision check using "collision_real_id" (see edit below), checking each pixel ahead of the player. If a wall is found, we collide. We get the bounding boxes of our wall and compare them to our player coordinates. As an example, if the player's x-coordinate is less than the wall's bbox_left coordinate, we set our snap_x to be the wall's bbox_left minus the player's x-offset from the right. That way if the player is moving right and we hit a wall from our right, we use the left side of the wall to correct the player's position and snap them to the wall. Then we do the same for all other directions: left, down, and up.
And we're done! Here are the results (player's move speed is still 0.99):
As you can see, the player is completely flush with the wall. No gaps, no overlaps, and our x and y coordinates are whole numbers. This is pixel-perfect.
Really that's all there is to it. You can insert this code into the "Step" event of the player, or just put it all into a script and call it from there.
Hope this tutorial helps and if you have any questions/comments, feel free to leave them down below.
EDIT: So I noticed that when working with very small speeds (below 0.25 I found), "instance_place" seems to not work as intended and the system breaks. I found the player "jumping" into position whenever they collide with a wall at a speed lower than 0.25 using this system. I think this is because there is a tolerance value applied to "instance_place" where the player has to be within the wall a certain amount of subpixels before the collision registers. Luckily, I've developed a solution that directly compares the bounding boxes of both the calling instance (player) and the colliding instance (wall) to get a precise collision without this tolerance value. It's a script I call "collision_real", and there's two versions: "collision_real(obj)", which simply returns true if there's a collision with a given object, and "collision_real_id(obj)", which returns the id of the colliding object upon collision.
Here is the code for both scripts:
To use, create a script in your project (name it whatever you want), then copy/paste the code into the script (or use the GitHub link above). This should fix this minor bug.
Other known issues:
- Does not take multiple collision objects into account
- Problems with image scaling with the player
Target Platform: All
Download: N/A
Links: Collision Code (GitHub)
Summary:
This tutorial is to help anyone who is having gaps/overlaps in their collision code. The purpose of this tutorial is to correct these gaps/overlaps by providing a solution using a pixel- and subpixel-perfect collision system.
Tutorial:
NOTE: This tutorial does NOT take diagonal/slope collisions into account. See my other tutorial on basic linear slopes.
If you have followed previous tutorials on collision code, then you should be familiar with how basic collisions are commonly set up.
GML:
if place_meeting(x+hspd,y,oWall) {
while !place_meeting(x+sign(hspd),y,oWall) {
x += sign(hspd);
}
hspd = 0;
}
x += hspd;
This code is fine and is certainly a way to check for collisions. However, it is not pixel-perfect. Let me explain why.
When we are moving at whole number increments (move speed does not contain a decimal), this system should run perfectly. No gaps, no overlaps. Completely pixel-perfect. Right? Well, no. Once we add fractional/decimal movement (such as friction, acceleration, and/or gravity), things start to get messy. You may find gaps/overlaps in your game, which isn't good because it can break the player experience. For example, the image below shows a player (white square) with a move speed of 0.99 colliding with the wall (red squares) using the collision system above. As you can probably tell, there are some issues. There's a gap, an overlap, and the x and y coordinates are not whole numbers, meaning the player is not flush with the wall.
The reason for this is because if we are moving at a fractional/decimal speed and we approach a wall using this collision code, the code will check to see if we are 0.99 pixels away from the wall, and if we are, then the "while" loop will move us forward one whole pixel. We don't want to move forward 1 pixel, we want to move 0.99 pixels so that we can be flush with the wall. We can attempt to fix this by making the rate at which we inch up to the wall smaller, but it still won't be quite as precise.
So how do we fix this? Well, I have a simple solution. We can "snap" the player to the wall before we collide with it, putting the player exactly where he needs to be. So if we approach a wall from our right, we can use the left side of the wall to match the right side of the player. To do this, we need to establish a few variables first.
GML:
var sprite_bbox_top = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
var sprite_bbox_bottom = sprite_get_bbox_bottom(sprite_index) - sprite_get_yoffset(sprite_index) + 1;
var sprite_bbox_left = sprite_get_bbox_left(sprite_index) - sprite_get_xoffset(sprite_index);
var sprite_bbox_right = sprite_get_bbox_right(sprite_index) - sprite_get_xoffset(sprite_index) + 1;
NOTE: If your collision mask differs from the sprite itself, change "sprite_index" to "mask_index". (Use Ctrl+F to find and replace)
Alright, so here is the code for our new collision system:
GML:
//Snap coordinates
snap_x = 0;
snap_y = 0;
//Collisions (no slopes)
//Horizontal
for(var h=0;h<=ceil(abs(hspd));h++) {//Necessary for overspeed
var wall_x = collision_real_id(h*sign(hspd),0,oWall);//See edit below for "collision_real_id" function
if wall_x != noone {
var wall_left = wall_x.bbox_left;
var wall_right = wall_x.bbox_right;
if x < wall_left {//right
snap_x = wall_left-sprite_bbox_right;
} else if x > wall_right {//left
snap_x = wall_right-sprite_bbox_left;
}
hspd = 0;
}
}
//Vertical
for(var v=0;v<=ceil(abs(vspd));v++) {//Necessary for overspeed
var wall_y = collision_real_id(0,v*sign(vspd),oWall);//See edit below for "collision_real_id" function
if wall_y != noone {
var wall_top = wall_y.bbox_top;
var wall_bottom = wall_y.bbox_bottom;
if y < wall_top {//down
snap_y = wall_top-sprite_bbox_bottom;
} else if y > wall_bottom {//up
snap_y = wall_bottom-sprite_bbox_top;
}
vspd = 0;
}
}
x += hspd;
y += vspd;
if snap_x != 0 x = snap_x;
if snap_y != 0 y = snap_y;
And we're done! Here are the results (player's move speed is still 0.99):
As you can see, the player is completely flush with the wall. No gaps, no overlaps, and our x and y coordinates are whole numbers. This is pixel-perfect.
Really that's all there is to it. You can insert this code into the "Step" event of the player, or just put it all into a script and call it from there.
Hope this tutorial helps and if you have any questions/comments, feel free to leave them down below.
EDIT: So I noticed that when working with very small speeds (below 0.25 I found), "instance_place" seems to not work as intended and the system breaks. I found the player "jumping" into position whenever they collide with a wall at a speed lower than 0.25 using this system. I think this is because there is a tolerance value applied to "instance_place" where the player has to be within the wall a certain amount of subpixels before the collision registers. Luckily, I've developed a solution that directly compares the bounding boxes of both the calling instance (player) and the colliding instance (wall) to get a precise collision without this tolerance value. It's a script I call "collision_real", and there's two versions: "collision_real(obj)", which simply returns true if there's a collision with a given object, and "collision_real_id(obj)", which returns the id of the colliding object upon collision.
Here is the code for both scripts:
GML:
///@arg x_offset
///@arg y_offset
///@arg obj
/*
- Checks for a collision with given object without the
added tolerance value applied to GM's "place_meeting"
- Returns true if collision with given object
- DO NOT PUT X OR Y FOR X_OFFSET OR Y_OFFSET!
*/
function collision_real(argument0,argument1,argument2) {
var x_offset = argument0;
var y_offset = argument1;
var obj = argument2;
var collision_detected = false;
for(var i=0;i<instance_number(obj);i++) {
var obj_id = instance_find(obj,i);
if bbox_top + y_offset < obj_id.bbox_bottom
&& bbox_left + x_offset < obj_id.bbox_right
&& bbox_bottom + y_offset > obj_id.bbox_top
&& bbox_right + x_offset > obj_id.bbox_left {
collision_detected = true;
}
}
return collision_detected;
}
GML:
///@arg x_offset
///@arg y_offset
///@arg obj
/*
- Checks for a collision between the colling instance
and the given object without the added tolerance value
applied to GM's "instance_place"
- Returns id of object upon collision
- DO NOT PUT X OR Y FOR X_OFFSET OR Y_OFFSET!
*/
function collision_real_id(argument0,argument1,argument2) {
var x_offset = argument0;
var y_offset = argument1;
var obj = argument2;
var collision_id = noone;
for(var i=0;i<instance_number(obj);i++) {
var obj_id = instance_find(obj,i);
if bbox_top + y_offset < obj_id.bbox_bottom
&& bbox_left + x_offset < obj_id.bbox_right
&& bbox_bottom + y_offset > obj_id.bbox_top
&& bbox_right + x_offset > obj_id.bbox_left {
collision_id = obj_id;
}
}
return collision_id;
}
To use, create a script in your project (name it whatever you want), then copy/paste the code into the script (or use the GitHub link above). This should fix this minor bug.
Other known issues:
- Does not take multiple collision objects into account
- Problems with image scaling with the player
Last edited: