This is a small post about a method for figuring out collision_line's "contact point" in GameMaker - in other words, obtaining not just whether there's something on the way, but also the nearest point on the nearest matching entity.
Most commonly desired for "hitscan" weapons and laser-sights, but has many uses.
Theory
The approach is a classic use case for binary search: the built-in function won't return a contact point, but we can call it multiple times, cutting the segment to check in half on each iteration until we have pixel precision.
So, for example, if we've checked and there's an object between the points, we'll check if there's anything between source point and halfway through.
If there is, we'll check between source point and 1/4 way through through next.
If there isn't, we'll check between halfway through and 3/4 way through next.
Each iteration halves the search space and after log2(distance)+1 iterations we have pixel precision.
Code (GM2022+ and LTS)
function collision_line_point(x1, y1, x2, y2, obj, prec, notme) { var rr = collision_line(x1, y1, x2, y2, obj, prec, notme); var rx = x2; var ry = y2; if (rr != noone) { var p0 = 0; var p1 = 1; repeat (ceil(log2(point_distance(x1, y1, x2, y2))) + 1) { var np = p0 + (p1 - p0) * 0.5; var nx = x1 + (x2 - x1) * np; var ny = y1 + (y2 - y1) * np; var px = x1 + (x2 - x1) * p0; var py = y1 + (y2 - y1) * p0; var nr = collision_line(px, py, nx, ny, obj, prec, notme); if (nr != noone) { rr = nr; rx = nx; ry = ny; p1 = np; } else p0 = np; } } return [rr, rx, ry]; }
The script returns an array containing hit instance ID as element 0, hit point X as element 1, and hit point Y as element 2. If there are no matching instances between points, instance ID is set to noone
while hit point XY are set to destination point.
It's used as following:
var r = collision_line_point(x, y, mouse_x, mouse_y, obj_some, true, true); draw_line(x, y, r[1], r[2]); if (r[0] != noone) { // r[0] holds the nearest (hit) instance. }
Code (GameMaker: Studio)
Note: enabling "fast collision system" in Global Game Settings helps a bunch - that makes GM use R-Trees for collision checking instead of picking through all of the instances each time.
/// collision_line_point(x1, y1, x2, y2, obj, prec, notme) var x1 = argument0; var y1 = argument1; var x2 = argument2; var y2 = argument3; var qi = argument4; var qp = argument5; var qn = argument6; var rr = collision_line(x1, y1, x2, y2, qi, qp, qn); var rx = x2; var ry = y2; if (rr != noone) { var p0 = 0; var p1 = 1; repeat (ceil(log2(point_distance(x1, y1, x2, y2))) + 1) { var np = p0 + (p1 - p0) * 0.5; var nx = x1 + (x2 - x1) * np; var ny = y1 + (y2 - y1) * np; var px = x1 + (x2 - x1) * p0; var py = y1 + (y2 - y1) * p0; var nr = collision_line(px, py, nx, ny, qi, qp, qn); if (nr != noone) { rr = nr; rx = nx; ry = ny; p1 = np; } else p0 = np; } } var r = array_create(3); r[0] = rr; r[1] = rx; r[2] = ry; return r;
Code (GameMaker 8.x)
Can't return arrays in vintage GM versions so the script stores the coordinates in a pair of global variables.
/// collision_line_point(x1, y1, x2, y2, obj, prec, notme) var x1; x1 = argument0; var y1; y1 = argument1; var x2; x2 = argument2; var y2; y2 = argument3; var qi; qi = argument4; var qp; qp = argument5; var qn; qn = argument6; var rr; rr = collision_line(x1, y1, x2, y2, qi, qp, qn); var rx; rx = x2; var ry; ry = y2; if (rr != noone) { var p0; p0 = 0; var p1; p1 = 1; repeat (ceil(log2(point_distance(x1, y1, x2, y2))) + 1) { var np; np = p0 + (p1 - p0) * 0.5; var nx; nx = x1 + (x2 - x1) * np; var ny; ny = y1 + (y2 - y1) * np; var px; px = x1 + (x2 - x1) * p0; var py; py = y1 + (y2 - y1) * p0; var nr; nr = collision_line(px, py, nx, ny, qi, qp, qn); if (nr != noone) { rr = nr; rx = nx; ry = ny; p1 = np; } else p0 = np; } } global.collision_line_point_x = rx; global.collision_line_point_y = ry; return rr;
And that's it. Have fun!
two questions.
1. Can i get more explanation on which part dose what?
2. Can i make it check for tiles instead of objects?
1. The post explains the logic, the code is just math (p0 is segment start, p1 is segment end, and we get x1y1x2y2 from those)
2. collision_line supports giving it tilemap IDs these days.
@Vadym amazing code, thank you so much! Worked like a charm for me even though I could hardly understand it before I started doing some refactoring 😅
@Modlich_303
I’ve created a gist with a bit of refactoring and comments that helped me understand what the code does. Hope it helps you too!
https://gist.github.com/4an70m/e32f36144bb436ca4a206c3366f7a173
I’m beyond stupid. Scratching my head and wondering for hours why the script isn’t working, just to found out I put the code in the step event and not draw event. Thank you so much!
For those with latest versions of gm2 (if the script isn’t working).
function collision_line_point(argument0,argument1,argument2,argument3,argument4,argument5,argument6)
{
*script code*
};
Great Stuff! Such an essential function
I dunno if you’ll ever see this again but thanks Vadim. This helped me immensely as I was trying to make a light that didn’t penetrate through walls properly.
Hi, sounds good but I still dont get it 100%. I tried it but “collision_line_point” is no valid function. And where do I have to put in the code, in a step event or in script (I´m relative new to GM).
Is it possible to hand over the gmx file of above shown video?!
BR
Timo
You add a script called “collision_line_point”, and paste the shown code into it. The demo shown in the GIF was made in GMLive rather than GM itself, so it would likely be a bit less intuitive.
Wonderfully more efficient than what I’ve been doing. This will help a great deal with optimization.
I’ve been using a for loop, with a direction to check every 4 pixels for 1024 pixels for a grappling hook. This will help a ton with optimization. Thanks c:
I said the same thing like twice, I’m way too tired xD sorry
Hey, that’s pretty smart. Like the way you think.