GML Pixel-Perfect Object-Based Collision

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.
GML:
if place_meeting(x+hspd,y,oWall) {
    while !place_meeting(x+sign(hspd),y,oWall) {
        x += sign(hspd);
    }
    hspd = 0;
}
x += hspd;
...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.

old collision example.png

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;
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:
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;
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):

new collision example.png

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

Snayff

Guest
Thanks for sharing this.

Please forgive my ignorance but what is the benefit of using code for collision checking over the collision event?
 
Please forgive my ignorance but what is the benefit of using code for collision checking over the collision event?
Ignorance forgiven. :p

The collision event only triggers when two collision masks meet. When using code for collision checking in the step event, it's triggered every frame. It's better to check for collisions before actually colliding with something so that the player has a better idea of when to stop moving. At least, this is the logic behind using the commonly coded collision system. My optimized collision system simply relocates the player when a collision does happen, but since we have to check the player's hspd beforehand, it must be coded in. That and using code is (generally) more faster & efficient overall. You have more control over using code than you do using events, given the situation.
 
Last edited:
S

Snayff

Guest
...using code is (generally) more faster & efficient overall.
Haha thanks, and thanks for coming back to me mate.

All things being equal, does that axiom hold for all events? I.e. at a certain point do you believe it is worth shifting the work on to custom functions?

At my current stage of GML infancy I think sticking with the native events makes sense but I expect as I progress I will run in to these hurdles so it is good to know about these things ahead of time!

P.s. the GIFs really helped clarify! the extra effort in including them is definitely appreciated!
 
do you believe it is worth shifting the work on to custom functions?
Indeed I do, but it's whatever works for you. I personally have never used the D&D system so I can't say GML is better, but hey if it works it works. When using GML though, you have more control over how and when things should happen based on what your game requires. Working with GML takes some getting used to at first, but it becomes easier with experience. I've reached a point where it feels "unnatural" to not work with code because I've grown so attached to it. I definitely recommend diving into GML a little more so you can have a better understanding of how everything works in GM and better understand why something isn't working right from a logical standpoint and attempt to fix it. I'm not saying you can't accomplish this with D&D, but with GML it's more "system-integrated" if that makes any sense.

P.s. the GIFs really helped clarify! the extra effort in including them is definitely appreciated!
Your welcome! ;)
 
E

Edmanbosch

Guest
Pretty good, nice tutorial. Your code is a bit messy though. Also, would it be better to put the snap code inside the actual wall collision?
 
E

Edmanbosch

Guest
I'm afraid not, since we must check ahead of the player before the collision, rather than when a collision is triggered. We're not relocating the player when we collide, but before we collide.
I've tried putting it in the collision code, and it works just fine. I'm still not sure what the problem of doing this is?
 
I've tried putting it in the collision code, and it works just fine. I'm still not sure what the problem of doing this is?
How did you go about doing it exactly? Did you mean making a collision event with the wall inside the player? I tried doing that and it doesn't seem to be working for me. Perhaps you left the code in collide_2()?
 
E

Edmanbosch

Guest
How did you go about doing it exactly? Did you mean making a collision event with the wall inside the player? I tried doing that and it doesn't seem to be working for me. Perhaps you left the code in collide_2()?
This was how I did it:
Code:
if (place_meeting(x + hsp, y, obj_wall)) {
    while (!place_meeting(x + sign(hsp), y, obj_wall)) {
        x += sign(hsp);
    }
 
    _wall = instance_place(x + sign(hsp), y, obj_wall);
    if (hsp > 0) {
        x = (_wall.bbox_left - 1) - _sprite_bbox_right;
        hsp = 0;
    }
    else {
        x = (_wall.bbox_right + 1) - _sprite_bbox_left;
        hsp = 0;
    }
 
    hsp = 0;
}
x += hsp;
And I did the same for the vertical collision.
 
This was how I did it:
Code:
if (place_meeting(x + hsp, y, obj_wall)) {
while (!place_meeting(x + sign(hsp), y, obj_wall)) {
x += sign(hsp);
}

_wall = instance_place(x + sign(hsp), y, obj_wall);
if (hsp > 0) {
x = (_wall.bbox_left - 1) - _sprite_bbox_right;
hsp = 0;
}
else {
x = (_wall.bbox_right + 1) - _sprite_bbox_left;
hsp = 0;
}

hsp = 0;
}
x += hsp;
And I did the same for the vertical collision.
Oookay I see what you did there, and hey that's one way to do it. To me it seems like extra work though, but I admit it is a bit easier to read than what I came up with.
 
How is it extra work?
I don't find it necessary to combine both collision systems honestly. I can see that one could work as a fail-safe for the other, but what I had looked at when I said it is when I saw this code:
while (!place_meeting(x + sign(hsp), y, obj_wall)) { x += sign(hsp); }
...
hsp = 0;
It's redundant to inch closer to the wall when the player is going to be relocated by the code that comes after it anyways, and hsp is already going to be 0 if we are next to the wall, so there's no need to set it to 0 at the end. Also, I keep getting this error:
___________________________________________
############################################################################################
FATAL ERROR in
action number 1
of Step Event0
for object Player:

Variable <unknown_object>.bbox_right(21, -2147483648) not set before reading it.
at gml_Script_collide_2 (line 19) - x = (wall.bbox_right + 1) - sprite_bbox_left;
############################################################################################
--------------------------------------------------------------------------------------------
stack frame is
gml_Script_collide_2 (line 19)
called from - gml_Object_Player_Step_0 (line 17) - collide_2();
What I believe is happening here is var "wall" is being set when the player detects a wall "hspd" pixels in front of them. Since var "wall" = instance_place(x+sign(hspd),y,obj_wall), the player detects a wall, but "wall" doesn't. This leads to the code trying to find the bbox_right of an object that isn't detected yet. Am I the only one getting this error?

Regardless, here's what I believe to be a better solution (and I'll update the thread after this post):
Code:
//Horizontal collisions
x += hspd;
if place_meeting(x+sign(hspd),y,Solid) {
    var wall = instance_place(x+sign(hspd),y,Solid);
    if hspd > 0 { //right
        x = (wall.bbox_left-1)-sprite_bbox_right;
    } else { //left
        x = (wall.bbox_right+1)-sprite_bbox_left;
    }
    hspd = 0;
}
...and the same vertically.
 
Last edited:
E

Edmanbosch

Guest
I prefer checking to see if the player is about to hit a wall and add hsp to the x after collisions(same for vertical), but yours works too.
 
S

Snayff

Guest
So I am using GML. I haven't used dnd at all. I am using a collision event and then putting the code in there. Am I right in thinking that your suggested method would be to create your own script to check for collision? Which presumably would be stored in a step event?
 
Am I right in thinking that your suggested method would be to create your own script to check for collision? Which presumably would be stored in a step event?
That'd be the way to do it! If the code is put into a script, it helps keep things organized and can be sometimes easier to debug (because you'll be able to find bugs faster when errors do occur.)

And yes, we want this in the step event because in addition to collision checking, we are also moving the player per frame, as suggested by the code "x+=hspd" and "y+=vspd" (and also because we can't really put it anywhere else in all honesty).
 
S

Snayff

Guest
I am trying to implement this but I am being dense... what is the Solid variable being looked for? Does it require the "solid" option to be ticked on walls?

Edit:
Changed it to look for the parent of my walls and it seems to work! Need to test properly but looking good, thanks buddy!
 
Last edited by a moderator:
M

MightyJo

Guest
Mate you might just wanna change this:
Code:
var sprite_bbox_top = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
And instead of using sprite_index tell the reader to use his mask image.

Some of us use animations, and using sprite_index there, makes my player spaz like there's no tomorrow. It gave me quite a few head scratches before I figured out what was going on. Otherwise brilliant code my friend. Kisses and smooches all around.
 
M

MightyJo

Guest
The mask is always the same. Your code gets the boundaries of the current sprite that is shown, not the boundaries of the mask. The sprite of the player is ever changing in width:



When my player is in that state and tries to collide with the wall he starts spazzing in front of it (also, just so you know, the animation is set to play only when x is either > or < than xprevious and to show a static sprite when they are =). Or maybe the problem is that the player's mask is one pixel smaller on each side (left and right) that the smallest animation sprite:



But I don't think that's the problem since your code only takes into account sprite boundaries, I think, right? But regardless of what the issue really is, changing sprite_index to mask_index solved the problem.
 
Last edited by a moderator:
But I don't think that's the problem since your code only takes into account sprite boundaries, I think, right? But regardless of what the issue really is, changing sprite_index to mask_index solved the problem.
The sprite_get_bbox functions get the bounding box sides relative to (0,0) of the sprite index. They take into account mask boundaries, obtained from the sprite itself. One thing to note is that these values are taken from the collision mask properties window of the sprite editor...
So according to this, sprite_get_bbox_top would return 0 here. If you're using a separate sprite as a collision box, maybe try replacing sprite_index with the index of the collision sprite? I replaced sprite_index with mask_index like you said and the player keeps going into the wall. That's an interesting error though...because I'm using this same collision system in another project that uses animations as well and I've never gotten this error. But hey if it works for you, it should be okay I guess.
 
C

Ceddil

Guest
I tried this and it jumps through the wall when going at it from the top or bottom. could you help?
Code:
//bbox variables
var sprite_bbox_top = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
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);
var sprite_bbox_bottom = sprite_get_bbox_bottom(sprite_index) - sprite_get_yoffset(sprite_index);

//Horizontal collisions
x += hspd;

//Snap
if place_meeting(x+sign(hspd),y,obj_solid) {
    var wall = instance_place(x+sign(hspd),y,obj_solid);
    if hspd > 0 { //right
        x = (wall.bbox_left-1)-sprite_bbox_right;
    } else { //left
        x = (wall.bbox_right+1)-sprite_bbox_left;
    }
    hspd = 0;
}

//vertical collisions
y += vspd;

//Snap
if place_meeting(x,y+sign(vspd),obj_solid) {
    var wall = instance_place(x,y+sign(vspd),obj_solid);
    if vspd > 0 { //top
        y = (wall.bbox_top-1)-sprite_bbox_top;
    } else { //bottom
        y = (wall.bbox_top+1)-sprite_bbox_bottom;
    }
    vspd = 0;
}
[CODE]
 
Last edited by a moderator:
Code:
//Snap
if place_meeting(x,y+sign(vspd),obj_solid) {
    var wall2 = instance_place(x,y+sign(vspd),obj_solid);
    if vspd > 0 { //top
        y = (wall2.bbox_top-1)-sprite_bbox_top;
    } else { //bottom
        y = (wall2.bbox_top+1)-sprite_bbox_bottom;
    }
    vspd = 0;
}
When vspd > 0 (meaning you're going down), you should be subtracting by sprite_bbox_bottom instead of sprite_bbox_top, because the bottom of the player should meet the top of the wall. As for when vspd < 0 (going up), it should be the same logic: The top of the player should be colliding with the bottom of the wall. It's important to not understand how something isn't working, but why it isn't. Doing so can strengthen your efficiency as a programmer so you can fix these kinds of errors on your own later on. Hope you got something out of this!
 
Last edited:
C

Ceddil

Guest
When vspd > 0 (meaning you're going down), you should be subtracting by sprite_bbox_bottom instead of sprite_bbox_top, because the bottom of the player should meet the top of the wall. As for when vspd < 0 (going up), it should be the same logic: The top of the player should be colliding with the bottom of the wall. It's important to not understand how something isn't working, but why it isn't. Doing so can strengthen your efficiency as a programmer so you can fix these kinds of errors on your own later on. Hope you got something out of this!
i understand why it isnt working, but i cant fix it. I dont know what i am doing wrong here. I re-wrote the code and it is working a little better, but i still need some help
Code:
//vertical collisions
y += vspd;

//Snap
if place_meeting(x,y+sign(vspd),obj_solid) {
    var wall = instance_place(x,y+sign(vspd),obj_solid);
    if vspd > 0 { //top
        y = (wall.bbox_top+1)-sprite_bbox_bottom;
    } else { //bottom
        y = (wall.bbox_bottom-1)-sprite_bbox_top;
    }
    vspd = 0;
}
this is in the step event of the player, and when i collide vertically it pushes me all he way to the right.
 
when i collide vertically it pushes me all he way to the right.
Because you're putting the player inside the wall. Next to wall.bbox_top and wall.bbox_bottom, flip the signs. It should be (wall.bbox_top-1), because we want to be a pixel above the top. Another thing to note is in GameMaker (or any other display for that matter), coordinate (0,0) is at the top left, not the bottom left like a standard graph. So if we go up, we are decreasing in y, but if we go down, we are increasing. It's weird, but that's just how it is.

So it would be (wall.bbox_top-1) and (wall.bbox_bottom+1).
 
C

Ceddil

Guest
Because you're putting the player inside the wall. Next to wall.bbox_top and wall.bbox_bottom, flip the signs. It should be (wall.bbox_top-1), because we want to be a pixel above the top. Another thing to note is in GameMaker (or any other display for that matter), coordinate (0,0) is at the top left, not the bottom left like a standard graph. So if we go up, we are decreasing in y, but if we go down, we are increasing. It's weird, but that's just how it is.

So it would be (wall.bbox_top-1) and (wall.bbox_bottom+1).
i did exactly what you said, and i dont know if i'm just bad at coding or math, but it didnt change anything.
Code:
//vertical collisions
y += vspd;

//Snap
if place_meeting(x,y+sign(vspd),obj_solid) {
    var wall = instance_place(x,y+sign(vspd),obj_solid);
    if vspd > 0 { //top
        y = (wall.bbox_top-1)-sprite_bbox_bottom;
    } else { //bottom
        y = (wall.bbox_bottom+1)-sprite_bbox_top;
    }
    vspd = 0;
}
and if this is the problem, here is the bounding box variables
Code:
//bbox variables
var sprite_bbox_top = sprite_get_bbox_top(sprite_index) - sprite_get_yoffset(sprite_index);
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);
var sprite_bbox_bottom = sprite_get_bbox_bottom(sprite_index) - sprite_get_yoffset(sprite_index);
 
i did exactly what you said, and i dont know if i'm just bad at coding or math, but it didnt change anything.
You didn't change your horizontal collisions at all? The vertical collisions are right as well as the bounding boxes, so it should be functional. If nothing else works, try switching "sprite_index" to "mask_index" in the bounding box vars. This seemed to work for @MightyJo so it may work for you. Also be sure your player has a mask as well as your walls.
 
M

Mister10

Guest
hi, I tried the code, and my character just falls through the floor. I used the whole mask index thing and it didn't fix it.
here's my code. I don't know if I'm doing anything wrong.

//bbox
var sprite_bbox_top = sprite_get_bbox_top(mask_index) - sprite_get_yoffset(mask_index);
var sprite_bbox_left = sprite_get_bbox_top(mask_index) - sprite_get_xoffset(mask_index);
var sprite_bbox_right = sprite_get_bbox_top(mask_index) - sprite_get_xoffset(mask_index);
var sprite_bbox_bottom = sprite_get_bbox_top(mask_index) - sprite_get_yoffset(mask_index);

//Horizontal collisions
x += hsp;

//Snap
if place_meeting(x+sign(hsp),y,obj_block) {
var wall = instance_place(x+sign(hsp),y,obj_block);
if hsp > 0 { //right
x = (wall.bbox_left-1)-sprite_bbox_right;
} else { //left
x = (wall.bbox_right+1)-sprite_bbox_left;
}
hsp = 0;
}
//Vertical collisions
y += vsp;

//Snap
if place_meeting(x,y+sign(vsp),obj_block) {
var wall = instance_place(x,y+sign(vsp),obj_block);
if vsp > 0 { //right
y = (wall.bbox_top-1)-sprite_bbox_bottom;
} else { //left
y = (wall.bbox_bottom+1)-sprite_bbox_top;
}
vsp = 0;
}
 

Bentley

Member
Thanks for the tutorial, it was very helpful. Thumbs up!

I just had a quick question.
First, we need to get the player's bounding box sides based on his origin. (The player doesn't need to be centered, but it is strongly recommended.)
Why do you recommend a centered origin as the code works for any origin (correct me if I'm wrong).
 
C

Christopher Rosa

Guest
I put all of the code in the step event of my player but when I land on obj_floor I glitch inside of it and start moving really fast to the right until there isnt any floor and then just fall.
 
R

ridiculoid

Guest
For anybody having trouble with this. There's a bug in this code. You need to change the "else" statements to "else if" statements. The "else" statements will trigger when the vspd or hspd is == 0. You're not going to want that. So instead of "else { //left", it needs to be "else if hsp < 0 { //left"
 
N

NeZvers

Guest
Lovely tutorial. I was frustrated with traditional collision system myself and this is similar to one of my collision test systems, only bit better.
I feel that this is a bit better way to do since you don't need loop to do counting.
 
N

NeZvers

Guest
Ok, I got time to revise my game code and I wanted to implement this collision system aaaand with that I found some improvements.
Since I don't like incomplete code shown, I'll lay it down so newcomers couldn't mess it up.
First thing - since I use same collision mask for all animations I SET IT AND FORGET IT in create event:

Code:
///Create_event

//I think this is better calculation for single mask
sprite_bbox_top = bbox_top - y;
sprite_bbox_bottom = bbox_bottom - y;
sprite_bbox_right = bbox_right - x;
sprite_bbox_left = bbox_left - x;
Code:
//Horizontal collisions
x += hspd;

//Snap
if place_meeting(x, y, oSolid) { // DON'T USE sign(hspd) since you are checking current possition
    var wall = instance_place(x+sign(hspd),y,oSolid); // same thing you are inside and don't want to check 1px more
    if (hspd > 0) { //right
        x = (wall.bbox_left-1)-sprite_bbox_right;
    } else if (hspd < 0) { // <<------ as it was pointed out need to have else if otherwise hspd=0 is included
        x = (wall.bbox_right+1)-sprite_bbox_left;
    }
    hspd = 0;
}

//Vertical collisions
y += vspd;

//Snap
if place_meeting(x, y, oSolid) {
    var wall = instance_place(x, y, oSolid);
    if (vspd > 0) { //down
        y = (wall.bbox_top-1) - sprite_bbox_bottom;
    } else if (vspd < 0) { //up
        y = (wall.bbox_bottom+1) - sprite_bbox_top;
    }
    vspd = 0;
}
This is now my go-to collision system because it survived vertically moving platforms spectacularly.
 
Last edited:
Woah this thread has gotten a lot of views! Much appreciation to those who have found bugs in the code, and I'll update the thread along with those fixes as well as the aforementioned diagonal capabilities. Also a few things:
Why do you recommend a centered origin as the code works for any origin (correct me if I'm wrong).
I guess it shouldn't matter too much, but I found it to be less buggy that way. It's a habit of mine that sorta bled through the thread so I apologize if this caused any confusion.
You need to change the "else" statements to "else if" statements. The "else" statements will trigger when the vspd or hspd is == 0. You're not going to want that. So instead of "else { //left", it needs to be "else if hsp < 0 { //left"
Thanks for this, I'll update the thread!
The mask is always the same. Your code gets the boundaries of the current sprite that is shown, not the boundaries of the mask.
Bit of an old quote but this is true. Didn't find the bug out myself until just recently, so expect an update!
 
C

CarlFoReal

Guest
I used this code and got an error saying the vspd was not set prior to reading it. chances are I'm just missing a variable but who knows. Any help would be appreciated given I've got minimal experience coding and collisions are the current problem. Thanks in advance.
 
J

Jumper

Guest
This code is awesome, works great for moving platforms. But my guy starts teleporting all over the place if he collides both horisontal and vertically. Is there any good workaround for that?

And to OP: Why do you (and nearly everyone else) post half the code and "Then we do the same vertically", and your demo project is an .exe and not a project file. This was easy enough code but man, everyone does that, its so annoying to spend 20 minutes of plotting and correcting code just to see if it works or not... Thanks for posting everything @NeZvers
 
N

NeZvers

Guest
@Jumper You can do whatever you want. You can do additional checks and do collision code if only one side is colliding.
 
D

DarklinkFighter

Guest
Shouldn't this way also include a while loop as well for the case that there are 2 (non player) colliders intersecting each other?

2019-01-11 14_39_50-Unbenannt - Paint.png

Without the loop it highly depends on Game Maker which of the Colliders gets detected, and in case it would be the wrong one it would snap the player inside the other one.
Also the loop would not increase performance to much because these cases are rather rare and only would loop in such cases.

Or do I understand the snippet wrong?
 
C

Cedric_of_Maroon

Guest
I was able to get pixel-perfect collisions by using the simpler method (judging based on speed rather than snapping) by just rounding the x and y coordinates after the collision every step:

x = round(x);
y = round(y);

Is there any downside to this? Seems to work fine.
 

Joe Ellis

Member
This is really good, and I hope people see this and think its the normal way to do it, rather than the stupid while loop,
The bbox vars are underrated, most simple collisions can be done with these and pretty much every old 2d game used to work like this.

You need to learn about vectors for the diagonal collisions though, there's quite a few simple things you can do which make the objects slide along lines perfectly. Learn about vertices, edge vectors between 2 vertices, cross products & dot products
 
- Fixed an issue where collisions weren't exactly pixel-perfect at player speeds lower than 0.25
- Created scripts "collide_real(obj)" and "collide_real_id(obj)". See edit for more details.

I'm currently working on linear slopes and should have a tutorial up soon! :)
 
- "collision_real" and "collision_real_id" scripts now accept x and y offsets.
- Rearranged the code to have the player move after the collision code as opposed to before it.
- Removed subtracting 1 from "sprite_bbox_right" and "sprite_bbox_bottom" when snapping the player and instead added 1 when initializing both.
- Small edits were made to tutorial description for better understanding.

I'm almost done with slopes. I still have to clean up the code and fix a few issues. The tutorial for it is going to be pretty lengthy, but I'll try my best to keep it concise and organized. So stay tuned! :)
 
Top