Recent

Author Topic: 2D collision routines  (Read 917 times)

Lulu

  • Sr. Member
  • ****
  • Posts: 330
2D collision routines
« on: April 10, 2025, 09:32:15 pm »
Hi, here a bunch of routine to check collision between primitive shape in 2D.
I use this routines in OGLCScene, a 2D game framework.

Original code (in Java?) can be found here: https://jeffreythompson.org/collision-detection/

Code: Pascal  [Select][+][-]
  1. unit Collision2D;
  2.  
  3. {
  4.  original code from https://jeffreythompson.org/collision-detection/
  5. }
  6.  
  7. {$mode ObjFPC}{$H+}
  8. {$modeswitch AdvancedRecords}
  9.  
  10. interface
  11.  
  12. uses Types;
  13.  
  14. type
  15.  
  16. ArrayOfTPointF = array of TPointF;
  17.  
  18. // For rectangular shape, use TRectF if you don't rotate your objects.
  19. // For rotated object, use instead Polygon.
  20.  
  21. TCollisionFunctions = record
  22.   function PointCircle(const aPt, aCenter: TPointF; const aRadius: single): boolean;
  23.   function PointRectF(const aPt: TPointF; const aR: TRectF): boolean;
  24.   function CircleCircle(const aCenter1: TPointF; const aRadius1: single;
  25.                         const aCenter2: TPointF; const aRadius2: single): boolean;
  26.   function CircleRectF(const aCenter: TPointF; const aRadius: single; const r: TRectF): boolean;
  27.   function RectFRectF(const r1, r2: TRectF): boolean;
  28.   function LinePoint(aLinePt1, aLinePt2, aPt: TPointF): boolean;
  29.   function LineLine(aLine1Pt1, aLine1Pt2, aLine2Pt1, aLine2Pt2: TPointF): boolean;
  30.   function LineCircle(aLinePt1, aLinePt2, aCircleCenter: TPointF; aRadius: single): boolean;
  31.   function LineRectF(aLinePt1, aLinePt2: TPointF; aRect: TRectF): boolean;
  32.  
  33.   function RectFContainLine(const r: TRectF; aLinePt1, aLinePt2: TPointF): boolean;
  34.   function CircleContainRectF(const aCenter: TPointF; const aRadius: single; const r: TRectF): boolean;
  35.  
  36.   function PolygonPoint(const aPts: ArrayOfTPointF; const aPt: TPointF): boolean;
  37.   function PolygonLine(const aPts: ArrayOfTPointF; const aLinePt1, aLinePt2: TPointF): boolean;
  38.   function PolygonCircle(const aPts: ArrayOfTPointF; const aCenter: TPointF; const aRadius: single): boolean;
  39.   function PolygonRectF(const aPts: ArrayOfTPointF; r: TRectF): boolean;
  40.   function PolygonPolygon(const aPts1, aPts2: ArrayOfTPointF): boolean;
  41. end;
  42.  
  43. // all functions are grouped into a record because Lazarus IDE
  44. // show them when we enter 'Collision.'
  45. // -> its easier to select the needed one without going to this unit every time.
  46. var
  47.   Collision: TCollisionFunctions;
  48.  
  49.  
  50. function PointF(aX, aY: single): TPointF;
  51. function Distance(aPt1, aPt2: TPointF): single;
  52. function DistanceSqr(aPt1, aPt2: TPointF): single;
  53.  
  54. implementation
  55.  
  56. function PointF(aX, aY: single): TPointF;
  57. begin
  58.   Result.x := aX;
  59.   Result.y := aY;
  60. end;
  61.  
  62. function Distance(aPt1, aPt2: TPointF): single;
  63. begin
  64.   Result := sqrt(sqr(aPt2.x-aPt1.x) + sqr(aPt2.y-aPt1.y));
  65. end;
  66.  
  67. function DistanceSqr(aPt1, aPt2: TPointF): single;
  68. begin
  69.   Result := sqr(aPt2.x-aPt1.x) + sqr(aPt2.y-aPt1.y);
  70. end;
  71.  
  72. function TCollisionFunctions.PointCircle(const aPt, aCenter: TPointF; const aRadius: single): boolean;
  73. begin
  74.   Result := DistanceSqr(aPt, aCenter) < Sqr(aRadius);
  75. end;
  76.  
  77. function TCollisionFunctions.PointRectF(const aPt: TPointF; const aR: TRectF): boolean;
  78. begin
  79.   Result := (aPt.x >= aR.Left) and (aPt.x < aR.Right) and
  80.             (aPt.y >= aR.Top) and (aPt.y < aR.Bottom);
  81. end;
  82.  
  83. function TCollisionFunctions.CircleCircle(const aCenter1: TPointF;
  84.   const aRadius1: single; const aCenter2: TPointF; const aRadius2: single): boolean;
  85. begin
  86.   Result := DistanceSqr(aCenter1, aCenter2) < Sqr(aRadius1 + aRadius2);
  87. end;
  88.  
  89. function TCollisionFunctions.CircleRectF(const aCenter: TPointF;
  90.   const aRadius: single; const r: TRectF): boolean;
  91. var closestX, closestY: single;
  92. begin
  93.   closestX := aCenter.x;
  94.   if closestX <= r.Left then closestX := r.Left
  95.     else if closestX > r.Right then closestX := r.Right;
  96.  
  97.   closestY := aCenter.y;
  98.   if closestY < r.Top then closestY := r.Top
  99.     else if closestY > r.Bottom then closestY := r.Bottom;
  100.  
  101.   Result := DistanceSqr(PointF(closestX, closestY), aCenter) < Sqr(aRadius);
  102. end;
  103.  
  104. function TCollisionFunctions.RectFRectF(const r1, r2: TRectF): boolean;
  105. begin
  106.   Result := (r1.Right >= r2.Left) and ( r1.Left <= r2.Right) and
  107.             (r1.Bottom >= r2.Top) and (r1.Top <= r2.Bottom);
  108. end;
  109.  
  110. function TCollisionFunctions.LinePoint(aLinePt1, aLinePt2, aPt: TPointF): boolean;
  111. var d1, d2, lineLen, buffer: single;
  112. begin
  113.   // get distance from the point to the two ends of the line
  114.   d1 := Distance(aPt, aLinePt1);
  115.   d2 := Distance(aPt, aLinePt2);
  116.   // get the length of the line
  117.   lineLen := Distance(aLinePt1, aLinePt2);
  118.   // since floats are so minutely accurate, add
  119.   // a little buffer zone that will give collision
  120.   buffer := 0.1;    // higher # = less accurate
  121.   // if the two distances are equal to the line's length, the point is on the line!
  122.   // note we use the buffer here to give a range, rather than one #
  123.   Result := (d1+d2 >= lineLen-buffer) and (d1+d2 <= lineLen+buffer);
  124. end;
  125.  
  126. function TCollisionFunctions.LineLine(aLine1Pt1, aLine1Pt2, aLine2Pt1, aLine2Pt2: TPointF): boolean;
  127. var uA, uB, x4_x3, y1_y3, y4_y3, x1_x3, x2_x1, y2_y1: single;
  128. begin
  129.   x4_x3 := aLine2Pt2.x - aLine2Pt1.x;
  130.   y1_y3 := aLine1Pt1.y - aLine2Pt1.y;
  131.   y4_y3 := aLine2Pt2.y - aLine2Pt1.y;
  132.   x1_x3 := aLine1Pt1.x - aLine2Pt1.x;
  133.   x2_x1 := aLine1Pt2.x - aLine1Pt1.x;
  134.   y2_y1 := aLine1Pt2.y - aLine1Pt1.y;
  135.   // calculate the distance to intersection point
  136.   uA := (x4_x3 * y1_y3 - y4_y3 * x1_x3) / (y4_y3 * x2_x1 - x4_x3 * y2_y1);
  137.   uB := (x2_x1 * y1_y3 - y2_y1 * x1_x3) / (y4_y3 * x2_x1 - x4_x3 * y2_y1);
  138.  
  139.   // if uA and uB are between 0-1, lines are colliding
  140.   Result := (uA >= 0) and (uA <= 1) and (uB >= 0) and (uB <= 1);
  141. end;
  142.  
  143. // from https://github.com/jeffThompson/CollisionDetection
  144. function TCollisionFunctions.LineCircle(aLinePt1, aLinePt2, aCircleCenter: TPointF; aRadius: single): boolean;
  145. var distX, distY, len, dot, closestX, closestY: single;
  146. begin
  147.   // is either end INSIDE the circle? if so, return true immediately
  148.   if Collision.PointCircle(aLinePt1, aCircleCenter, aRadius) or
  149.      Collision.PointCircle(aLinePt2, aCircleCenter, aRadius)  then exit(True);
  150.   // get length of the line
  151.   distX := aLinePt1.x - aLinePt2.x;
  152.   distY := aLinePt1.y - aLinePt2.y;
  153.   len := sqrt((distX*distX) + (distY*distY));
  154.   // get dot product of the line and circle
  155.   dot := (((aCircleCenter.x-aLinePt1.x)*(aLinePt2.x-aLinePt1.x)) +
  156.          ((aCircleCenter.y-aLinePt1.y)*(aLinePt2.y-aLinePt1.y))) / (len*len);
  157.   // find the closest point on the line
  158.   closestX := aLinePt1.x + (dot * (aLinePt2.x-aLinePt1.x));
  159.   closestY := aLinePt1.y + (dot * (aLinePt2.y-aLinePt1.y));
  160.   // is this point actually on the line segment? if so keep going, but if not, return false
  161.   if not Collision.LinePoint(aLinePt1, aLinePt2, PointF(closestX,closestY)) then exit(False);
  162.   // get distance to closest point
  163.   distX := closestX - aCircleCenter.x;
  164.   distY := closestY - aCircleCenter.y;
  165.   Result := (distX*distX) + (distY*distY) <= Sqr(aRadius);
  166. end;
  167.  
  168. function TCollisionFunctions.LineRectF(aLinePt1, aLinePt2: TPointF; aRect: TRectF): boolean;
  169. begin
  170.   // check if the rect contain the line
  171.   if PointRectF(aLinePt1, aRect) then exit(True);
  172.   // check if the line has hit any of the rectangle's sides
  173.   // uses the Line/Line function
  174.   Result := Collision.LineLine(aLinePt1, aLinePt2, aRect.TopLeft, PointF(aRect.Left, aRect.Bottom)) or
  175.             Collision.LineLine(aLinePt1, aLinePt2, PointF(aRect.Right, aRect.Top), aRect.BottomRight) or
  176.             Collision.LineLine(aLinePt1, aLinePt2, aRect.TopLeft, PointF(aRect.Right, aRect.Top)) or
  177.             Collision.LineLine(aLinePt1, aLinePt2, PointF(aRect.Left, aRect.Bottom), aRect.BottomRight);
  178. end;
  179.  
  180. function TCollisionFunctions.RectFContainLine(const r: TRectF; aLinePt1, aLinePt2: TPointF): boolean;
  181. begin
  182.   Result := PointRectF(aLinePt1, r) and PointRectF(aLinePt2, r);
  183. end;
  184.  
  185. function TCollisionFunctions.CircleContainRectF(const aCenter: TPointF;
  186.   const aRadius: single; const r: TRectF): boolean;
  187. begin
  188.   Result := PointCircle(r.TopLeft, aCenter, aRadius) and
  189.             PointCircle(PointF(r.Right, r.Top), aCenter, aRadius) and
  190.             PointCircle(r.BottomRight, aCenter, aRadius) and
  191.             PointCircle(PointF(r.Left, r.Bottom), aCenter, aRadius);
  192. end;
  193.  
  194. function TCollisionFunctions.PolygonPoint(const aPts: ArrayOfTPointF; const aPt: TPointF): boolean;
  195. var next, current: integer;
  196.   vc, vn: TPointF;
  197. begin
  198.   Result := False;
  199.   if Length(aPts) = 0 then exit;
  200.   for current :=0 to High(aPts) do begin
  201.     next := current + 1;
  202.     if next = Length(aPts) then next := 0;
  203.     vc := aPts[current];
  204.     vn := aPts[next];
  205.     if ((vc.y > aPt.y) <> (vn.y > aPt.y)) and (aPt.x < (vn.x-vc.x) * (aPt.y-vc.y) / (vn.y-vc.y) + vc.x)
  206.       then Result := not Result;
  207.   end;
  208. end;
  209.  
  210. function TCollisionFunctions.PolygonLine(const aPts: ArrayOfTPointF;
  211.   const aLinePt1, aLinePt2: TPointF): boolean;
  212. var current: integer;
  213. begin
  214.   if Length(aPts) = 0 then exit(False);
  215.   current := 0;
  216.   while current < High(aPts)-1 do begin
  217.     if LineLine(aPts[current], aPts[current+1], aLinePt1, aLinePt2) then exit(True);
  218.     inc(current, 2);
  219.   end;
  220.   // optional: check if line is inside the polygon
  221.   Result := PolygonPoint(aPts, aLinePt1);
  222. end;
  223.  
  224. function TCollisionFunctions.PolygonCircle(const aPts: ArrayOfTPointF;
  225.   const aCenter: TPointF; const aRadius: single): boolean;
  226. var current: integer;
  227. begin
  228.  if Length(aPts) = 0 then exit(False);
  229.   current := 0;
  230.   while current < High(aPts)-1 do begin
  231.     if LineCircle(aPts[current], aPts[current+1], aCenter, aRadius) then exit(True);
  232.     inc(current, 2);
  233.   end;
  234.   // optional:
  235.   if PolygonPoint(aPts, aCenter) then exit(True); // check if the circle is inside the polygon
  236.   Result := PointCircle(aPts[0], aCenter, aRadius); // check if the polygon is inside the circle
  237. end;
  238.  
  239. function TCollisionFunctions.PolygonRectF(const aPts: ArrayOfTPointF; r: TRectF): boolean;
  240. var current: integer;
  241. begin
  242.   if Length(aPts) = 0 then exit(False);
  243.   current := 0;
  244.   while current < High(aPts)-1 do begin
  245.     if LineRectF(aPts[current], aPts[current+1], r) then exit(True);
  246.     inc(current, 2);
  247.   end;
  248.   // optional:
  249.   if PolygonPoint(aPts, r.TopLeft) then exit(True); // check if the rectangle is inside the polygon
  250.   Result := PointRectF(aPts[0], r); // check if the polygon is inside the rectangle
  251. end;
  252.  
  253. function TCollisionFunctions.PolygonPolygon(const aPts1, aPts2: ArrayOfTPointF): boolean;
  254. var current: integer;
  255. begin
  256.   if (Length(aPts1) = 0)  or (Length(aPts2) = 0) then exit(False);
  257.   current := 0;
  258.   while current < High(aPts1)-1 do begin
  259.     if PolygonLine(aPts2, aPts1[current], aPts1[current+1]) then exit(True);
  260.     inc(current, 2);
  261.   end;
  262.   // optional:
  263.   if PolygonPoint(aPts1, aPts2[0]) then exit(True); // check if the polygon2 is inside polygon1
  264.   Result := PolygonPoint(aPts2, aPts1[0]); // check if the polygon1 is inside polygon2
  265. end;
  266.  
  267. end.
  268.  

Edit: the code has been improved according to mm7's remark
« Last Edit: April 11, 2025, 08:47:21 pm by Lulu »
wishing you a nice life!
GitHub repositories https://github.com/Lulu04

Guva

  • Full Member
  • ***
  • Posts: 197
  • 🌈 ZX-Spectrum !!!
Re: 2D collision routines
« Reply #1 on: April 11, 2025, 02:36:00 am »
Nice !!! thank for sharing.

There is also a good example of collision checking for a sprite with pixel-by-pixel sampling, you might find it useful.
https://github.com/GalaxyShad/Pascal-Sonic
https://www.youtube.com/watch?v=3PAmUILrFGw
« Last Edit: April 11, 2025, 02:48:07 am by Guva »

flowCRANE

  • Hero Member
  • *****
  • Posts: 937
Re: 2D collision routines
« Reply #2 on: April 11, 2025, 01:34:34 pm »
Code: Pascal  [Select][+][-]
  1. // all functions are grouped into a record because Lazarus IDE
  2. // show them when we enter 'Collision.'
  3. // -> its easier to select the needed one without going to this unit every time.
  4. var
  5.   Collision: TCollisionFunctions;
  6.  

You're senselessly over-engineering the code instead of keeping it as simple as possible. Please don't do this, just declare these functions normally and remove everything that is unnecessary, i.e. compiler switches, record, global variable. If someone needs namespaces that will make it easier to filter the completion box, they can always type the module name and after the dot they will have a list of functions (because this module contains practically only functions). Apart from that, let the users use this API the way they wants, without imposing anything, especially unnecessary syntactic sugar that practically does not help at all and only obfuscates the code.

The second issue is code formatting — it needs to be corrected, especially if statements and local variable blocks. It is customary in Pascal to have prefixes in uppercase letters — T for types, P for pointers, A for function arguments, I for interfaces. In your case, arguments have a prefix that is a lowercase a, which is inconsistent with the accepted naming convention. And don't use abbreviated argument and variable names, use full ones. For example, you have a function whose argument to pass an array of points is called aPts — use the full name with the correct prefix, i.e. APoints.



Thanks for posting the code — features like this are very useful and I'm sure many will benefit from them.
« Last Edit: April 11, 2025, 01:36:05 pm by flowCRANE »
Lazarus 4.2 with FPC 3.2.2, Windows 11 — all 64-bit

Working solo on a retro-style action/adventure game (pixel art), programming the engine from scratch, using Free Pascal and SDL3.

mm7

  • Full Member
  • ***
  • Posts: 227
  • PDP-11 RSX Pascal, Turbo Pascal, Delphi, Lazarus
Re: 2D collision routines
« Reply #3 on: April 11, 2025, 03:44:20 pm »
Hi Lulu
just for added performance I would remove sqrt().

Code: Pascal  [Select][+][-]
  1. function DistanceSqr(aPt1, aPt2: TPointF): single;
  2. begin
  3.    Result := sqr(aPt2.x-aPt1.x)+sqr(aPt2.y-aPt1.y);
  4. end;
  5.  
  6. function TCollisionFunctions.PointCircle(const aPt, aCenter: TPointF; const aRadius: single): boolean;
  7. begin
  8.   Result := DistanceSqr(aPt, aCenter) < sqr(aRadius);
  9. end;
  10.  

Lulu

  • Sr. Member
  • ****
  • Posts: 330
Re: 2D collision routines
« Reply #4 on: April 11, 2025, 08:22:38 pm »
@Guva: thank you, I will take a look.

@flowCRANE: thank you for your comments, I'll be more careful next time for the code formatting.
About the encapsulation into a record, the code is just a bunch of functions, its easy to adapt according to one's needs.

@mm7: remove sqrt is a good idea! I will apply your changes. Thank you!
wishing you a nice life!
GitHub repositories https://github.com/Lulu04

 

TinyPortal © 2005-2018