Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Rectangle Collisions Demo using SAT
#1
Hi all, 

I love this kinda stuff so whilst im now onto incorporating circles and polygons into this, for now heres rectangles! 

SAT btw is separated axis theory, basically if you can draw a line between two things then they arent colliding, if you cant they are...its faster acurate and easily expandable to fit almost complex shapes....

This demo runs as is, needs no files or anyting for once! 

Code: (Select All)
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'// Unseen GDK 3 -  Collisions Dev - Rects Only For now Just To Demo \\
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
RANDOMIZE TIMER
'// REM $INCLUDE:'UnseenGDK2\GDK2.bi'

CONST GDK_Vector_Size = 8
CONST GDK_TRUE = -1, GDK_FALSE = 0
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////// System Initialisation ////////////////////////////////////////////////
'/GDK2_System_New "UnseenGDK2\Projects\", 800, 600, GDK_FALSE, "GDK Rectangle Demo"
SCREEN _NEWIMAGE(800, 600, 32)
_SCREENMOVE 0, 0
_DELAY 1

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

CONST MAX_BOXES = 10

CONST FLASH_TIME = 0.5
DIM SHARED Boxes(MAX_BOXES) AS GDK_Box

DIM SHARED GT#, LastGT#
GT# = TIMER(.001): LastGT# = GT#
InitBoxes


'//////////////////////////////////////// Main Loop ////////////////////////////////////////////////////////////
DO

  GT# = TIMER(.001)
  dt = GT# - LastGT#
  UpdateBoxes dt

  CLS

  FOR I = 0 TO MAX_BOXES - 1
    IF Boxes(I).FlashTimer > 0 THEN
      GDK_DrawBox Boxes(I), _RGB32(255, 255, 0)
    ELSE
      GDK_DrawBox Boxes(I), Boxes(I).pColor
    END IF
  NEXT I


  _DISPLAY
  LastGT# = GT#
LOOP UNTIL INKEY$ = CHR$(27)

SYSTEM

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'REM $INCLUDE:'UnseenGDK2\GDK2.bm'
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

TYPE GDK_Vector
  X AS SINGLE
  Y AS SINGLE
END TYPE

'The new GDK_Box type for a rotated rectangle, without pre-calculated corners
TYPE GDK_Box
  Position AS GDK_Vector
  WidthHeight AS GDK_Vector
  Rotation AS SINGLE
  RotationPointOffset AS GDK_Vector
  Rotation_Speed AS SINGLE ' New: For rotational speed
  Velocity AS GDK_Vector ' Movement vector for this box
  pColor AS LONG ' Color for drawing
  IsColliding AS INTEGER ' Flag to indicate collision state
  FlashTimer AS SINGLE ' Timer for collision flash
END TYPE


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Draws a box using its calculated corner vertices.
SUB GDK_DrawBox (Box AS GDK_Box, bcolor AS LONG)
  DIM Corners(3) AS GDK_Vector
  CALL GDK_GetBoxCorners(Box, Corners())
  LINE (Corners(0).X, Corners(0).Y)-(Corners(1).X, Corners(1).Y), bcolor
  LINE (Corners(1).X, Corners(1).Y)-(Corners(2).X, Corners(2).Y), bcolor
  LINE (Corners(2).X, Corners(2).Y)-(Corners(3).X, Corners(3).Y), bcolor
  LINE (Corners(3).X, Corners(3).Y)-(Corners(0).X, Corners(0).Y), bcolor
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Rotates a point around a pivot point by a given angle (in radians)
SUB GDK_RotatePoint (Result AS GDK_Vector, p AS GDK_Vector, pivot AS GDK_Vector, angle AS SINGLE)
  DIM temp_x AS SINGLE, temp_y AS SINGLE
  temp_x = p.X - pivot.X
  temp_y = p.Y - pivot.Y
  Result.X = temp_x * COS(angle) - temp_y * SIN(angle)
  Result.Y = temp_x * SIN(angle) + temp_y * COS(angle)
  Result.X = Result.X + pivot.X
  Result.Y = Result.Y + pivot.Y
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Calculates the corner vertices for a given box
SUB GDK_GetBoxCorners (Box AS GDK_Box, Corners() AS GDK_Vector)
  DIM HalfW AS SINGLE, HalfH AS SINGLE
  DIM UnrotatedCorner AS GDK_Vector
  DIM Center AS GDK_Vector

  HalfW = Box.WidthHeight.X / 2
  HalfH = Box.WidthHeight.Y / 2
  Center = Box.Position

  UnrotatedCorner.X = Center.X - HalfW
  UnrotatedCorner.Y = Center.Y - HalfH
  CALL GDK_RotatePoint(Corners(0), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X + HalfW
  UnrotatedCorner.Y = Center.Y - HalfH
  CALL GDK_RotatePoint(Corners(1), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X + HalfW
  UnrotatedCorner.Y = Center.Y + HalfH
  CALL GDK_RotatePoint(Corners(2), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X - HalfW
  UnrotatedCorner.Y = Center.Y + HalfH
  CALL GDK_RotatePoint(Corners(3), UnrotatedCorner, Center, Box.Rotation)
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for overlap of projections on a given axis
FUNCTION GDK_Overlap_On_Axis (Corners1() AS GDK_Vector, Corners2() AS GDK_Vector, Axis AS GDK_Vector)
  DIM Min1 AS SINGLE, Max1 AS SINGLE, Min2 AS SINGLE, Max2 AS SINGLE
  DIM Projection AS SINGLE, I AS LONG

  Min1 = 9999999!: Max1 = -9999999!
  Min2 = 9999999!: Max2 = -9999999!

  FOR I = 0 TO 3
    Projection = Corners1(I).X * Axis.X + Corners1(I).Y * Axis.Y
    IF Projection < Min1 THEN Min1 = Projection
    IF Projection > Max1 THEN Max1 = Projection
  NEXT I

  FOR I = 0 TO 3
    Projection = Corners2(I).X * Axis.X + Corners2(I).Y * Axis.Y
    IF Projection < Min2 THEN Min2 = Projection
    IF Projection > Max2 THEN Max2 = Projection
  NEXT I

  IF Max1 >= Min2 AND Max2 >= Min1 THEN GDK_Overlap_On_Axis = -1 ELSE GDK_Overlap_On_Axis = 0
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for intersection using SAT
FUNCTION GDK_Box_Intersect (Box1 AS GDK_Box, Box2 AS GDK_Box)
  DIM Box1_Corners(3) AS GDK_Vector, Box2_Corners(3) AS GDK_Vector
  DIM Axis(3) AS GDK_Vector, length AS SINGLE, I AS LONG

  CALL GDK_GetBoxCorners(Box1, Box1_Corners())
  CALL GDK_GetBoxCorners(Box2, Box2_Corners())

  Axis(0).X = -(Box1_Corners(1).Y - Box1_Corners(0).Y): Axis(0).Y = Box1_Corners(1).X - Box1_Corners(0).X
  Axis(1).X = -(Box1_Corners(0).Y - Box1_Corners(3).Y): Axis(1).Y = Box1_Corners(0).X - Box1_Corners(3).X

  FOR I = 0 TO 1
    length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
    IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
    IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
  NEXT I

  Axis(2).X = -(Box2_Corners(1).Y - Box2_Corners(0).Y): Axis(2).Y = Box2_Corners(1).X - Box2_Corners(0).X
  Axis(3).X = -(Box2_Corners(0).Y - Box2_Corners(3).Y): Axis(3).Y = Box2_Corners(0).X - Box2_Corners(3).X

  FOR I = 2 TO 3
    length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
    IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
    IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
  NEXT I

  GDK_Box_Intersect = -1
END FUNCTION


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'// DEMO For Simple Box collision
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' --- Main Logic ---
SUB InitBoxes
  DIM I AS LONG, W AS SINGLE, H AS SINGLE, R AS SINGLE, S AS SINGLE
  FOR I = 0 TO MAX_BOXES - 1
    W = 10 + RND * 50
    H = 10 + RND * 50
    R = RND * 2 * _PI
    S = 10 + RND * 50
    Boxes(I).Position.X = RND * 800
    Boxes(I).Position.Y = RND * 600 '_HEIGHT
    Boxes(I).WidthHeight.X = W
    Boxes(I).WidthHeight.Y = H
    Boxes(I).Rotation = R
    Boxes(I).Rotation_Speed = (RND * .3)
    Boxes(I).RotationPointOffset.X = W / 2: Boxes(I).RotationPointOffset.Y = H / 2
    Boxes(I).Velocity.X = (RND * 5) * S
    Boxes(I).Velocity.Y = (RND * 5) * S
    Boxes(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)
    Boxes(I).IsColliding = GDK_FALSE
    Boxes(I).FlashTimer = 0
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB UpdateBoxes (dt AS SINGLE)
  DIM I AS LONG, J AS LONG
  DIM Box_Hit AS INTEGER
  ' Screen wrapping
  DIM BoxLeft AS SINGLE, BoxRight AS SINGLE, BoxTop AS SINGLE, BoxBottom AS SINGLE
  DIM tempX AS SINGLE, tempY AS SINGLE

  FOR I = 0 TO MAX_BOXES - 1
    ' Update position and rotation
    Boxes(I).Position.X = Boxes(I).Position.X + Boxes(I).Velocity.X * dt
    Boxes(I).Position.Y = Boxes(I).Position.Y + Boxes(I).Velocity.Y * dt
    Boxes(I).Rotation = Boxes(I).Rotation + Boxes(I).Rotation_Speed * dt

    BoxLeft = Boxes(I).Position.X - Boxes(I).WidthHeight.X / 2
    BoxRight = Boxes(I).Position.X + Boxes(I).WidthHeight.X / 2
    BoxTop = Boxes(I).Position.Y - Boxes(I).WidthHeight.Y / 2
    BoxBottom = Boxes(I).Position.Y + Boxes(I).WidthHeight.Y / 2

    IF BoxRight < 0 THEN Boxes(I).Position.X = _WIDTH + Boxes(I).WidthHeight.X / 2
    IF BoxLeft > _WIDTH THEN Boxes(I).Position.X = 0 - Boxes(I).WidthHeight.X / 2
    IF BoxBottom < 0 THEN Boxes(I).Position.Y = _HEIGHT + Boxes(I).WidthHeight.Y / 2
    IF BoxTop > _HEIGHT THEN Boxes(I).Position.Y = 0 - Boxes(I).WidthHeight.Y / 2

    ' Check for collision with other boxes
    Box_Hit = GDK_FALSE
    FOR J = I + 1 TO MAX_BOXES - 1
      IF GDK_Box_Intersect(Boxes(I), Boxes(J)) THEN
        Box_Hit = GDK_TRUE
        ' Simple bounce logic
        tempX = Boxes(I).Velocity.X: tempY = Boxes(I).Velocity.Y
        Boxes(I).Velocity.X = Boxes(J).Velocity.X: Boxes(I).Velocity.Y = Boxes(J).Velocity.Y
        Boxes(J).Velocity.X = tempX: Boxes(J).Velocity.Y = tempY
        Boxes(J).FlashTimer = FLASH_TIME
      END IF
    NEXT J

    ' Manage flash timer
    IF Box_Hit THEN
      Boxes(I).FlashTimer = FLASH_TIME
    ELSEIF Boxes(I).FlashTimer > 0 THEN
      Boxes(I).FlashTimer = Boxes(I).FlashTimer - dt
    END IF
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_ClosestPointOnBox (Result AS GDK_Vector, Box AS GDK_Box, vPoint AS GDK_Vector)
  DIM Box_Corners(3) AS GDK_Vector
  CALL GDK_GetBoxCorners(Box, Box_Corners())

  DIM Closest AS GDK_Vector
  DIM I AS LONG, J AS LONG
  DIM MinDistanceSquared AS SINGLE, CurrentDistanceSquared AS SINGLE

  MinDistanceSquared = 9999999!

  FOR I = 0 TO 3
    J = (I + 1) MOD 4
    DIM SegmentStart AS GDK_Vector, SegmentEnd AS GDK_Vector
    SegmentStart = Box_Corners(I)
    SegmentEnd = Box_Corners(J)

    DIM dx AS SINGLE, dy AS SINGLE
    dx = SegmentEnd.X - SegmentStart.X
    dy = SegmentEnd.Y - SegmentStart.Y

    DIM t AS SINGLE
    t = ((vPoint.X - SegmentStart.X) * dx + (vPoint.Y - SegmentStart.Y) * dy) / (dx * dx + dy * dy)

    IF t < 0 THEN t = 0
    IF t > 1 THEN t = 1

    DIM ClosestPointOnSegment AS GDK_Vector
    ClosestPointOnSegment.X = SegmentStart.X + t * dx
    ClosestPointOnSegment.Y = SegmentStart.Y + t * dy

    CurrentDistanceSquared = (vPoint.X - ClosestPointOnSegment.X) ^ 2 + (vPoint.Y - ClosestPointOnSegment.Y) ^ 2

    IF CurrentDistanceSquared < MinDistanceSquared THEN
      MinDistanceSquared = CurrentDistanceSquared
      Closest = ClosestPointOnSegment
    END IF
  NEXT I

  Result = Closest
END SUB

Unseen
Reply
#2
Quote:SAT btw is separated axis theory, basically if you can draw a line between two things then they arent colliding, if you cant they are...its faster acurate and easily expandable to fit almost complex shapes....

Rectangle intersects easy as eating pie, but collisions with irregular objects is the cats tail (the center of happiness for a cat, if you listen to Wayne Dyer any).

Very cool idea! I gots to study to see how this is accomplished, thanks! Your +1 in madscijr thread where this topic arose. Smile
  724  855  599  923  575  468  400  206  147  564  878  823  652  556 bxor cross forever
Reply
#3
Very cool!

Why is it sometimes two rectangles get hooked together? Inquiring minds want to know... and I wouldn't mind finding out, either.

+1

Pete
Reply
#4
Quote:Why is it sometimes two rectangles get hooked together? Inquiring minds want to know... and I wouldn't mind finding out, either.
If they start overlapped (they're randomly positioned) and have similar velocities they will appear to be grouped...

Quote:Very cool idea! I gots to study to see how this is accomplished, thanks! Your +1 in madscijr thread where this topic arose. 
Thanks buddy! Glad you like it.

Unseen
Reply
#5
Code: (Select All)
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'// Unseen GDK 3 - Collisions Dev - Rects and circles\\
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
RANDOMIZE TIMER
'// REM $INCLUDE:'UnseenGDK2\GDK2.bi'

CONST GDK_Vector_Size = 8
CONST GDK_TRUE = -1, GDK_FALSE = 0
'///////////////////////////////////// System Initialisation ////////////////////////////////////////////////
'/GDK2_System_New "UnseenGDK2\Projects\", 800, 600, GDK_FALSE, "GDK Rectangle Demo"
SCREEN _NEWIMAGE(800, 600, 32)
_SCREENMOVE 0, 0
_DELAY 1

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

CONST MAX_BOXES = 20
CONST MAX_CIRCLES = 40

CONST FLASH_TIME = 0.25
DIM SHARED Boxes(MAX_BOXES) AS GDK_Box
DIM SHARED Circles(MAX_CIRCLES) AS GDK_Circle
DIM SHARED Bxs, Crc, Ply

DIM SHARED GT#, LastGT#
GT# = TIMER(.001): LastGT# = GT#
InitCircles
InitBoxes

Crc = GDK_TRUE
'Bxs = GDK_TRUE


'//////////////////////////////////////// Main Loop ////////////////////////////////////////////////////////////
DO
_LIMIT 30
_FPS 30

GT# = TIMER(.001)
dt = GT# - LastGT#
IF Crc THEN UpdateCircles dt
IF Bxs THEN UpdateBoxes dt
IF Crc AND Bxs THEN HandleBoxCircleCollisions dt


LINE (0, 0)-(_WIDTH, _HEIGHT), _RGBA32(0, 0, 0, 30), BF

IF Crc THEN DrawCircles
IF Bxs THEN DrawBoxes

_DISPLAY
LastGT# = GT#
LOOP UNTIL INKEY$ = CHR$(27)

SYSTEM

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'REM $INCLUDE:'UnseenGDK2\GDK2.bm'
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

TYPE GDK_Vector
X AS SINGLE
Y AS SINGLE
END TYPE

'The new GDK_Box type for a rotated rectangle, without pre-calculated corners
TYPE GDK_Box
Position AS GDK_Vector
WidthHeight AS GDK_Vector
Rotation AS SINGLE
RotationPointOffset AS GDK_Vector
Rotation_Speed AS SINGLE ' New: For rotational speed
Velocity AS GDK_Vector ' Movement vector for this box
pColor AS LONG ' Color for drawing
IsColliding AS INTEGER ' Flag to indicate collision state
FlashTimer AS SINGLE ' Timer for collision flash
Mass AS SINGLE
END TYPE

TYPE GDK_Circle
Position AS GDK_Vector ' Position vector for the circle's center
Radius AS SINGLE ' Radius of the circle
Rotation AS SINGLE ' Rotational property, kept as circles can still spin
Rotation_Speed AS SINGLE ' Rotational speed
Velocity AS GDK_Vector ' Movement vector for this circle
pColor AS LONG ' Color for drawing
IsColliding AS INTEGER ' Flag to indicate collision state
FlashTimer AS SINGLE ' Timer for collision flash
Mass AS SINGLE
END TYPE


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Draws a box using its calculated corner vertices.
SUB GDK_DrawBox (Box AS GDK_Box, bcolor AS LONG)
DIM Corners(3) AS GDK_Vector
CALL GDK_GetBoxCorners(Box, Corners())
LINE (Corners(0).X, Corners(0).Y)-(Corners(1).X, Corners(1).Y), bcolor
LINE (Corners(1).X, Corners(1).Y)-(Corners(2).X, Corners(2).Y), bcolor
LINE (Corners(2).X, Corners(2).Y)-(Corners(3).X, Corners(3).Y), bcolor
LINE (Corners(3).X, Corners(3).Y)-(Corners(0).X, Corners(0).Y), bcolor
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Rotates a point around a pivot point by a given angle (in radians)
SUB GDK_RotatePoint (Result AS GDK_Vector, p AS GDK_Vector, pivot AS GDK_Vector, angle AS SINGLE)
DIM temp_x AS SINGLE, temp_y AS SINGLE
temp_x = p.X - pivot.X
temp_y = p.Y - pivot.Y
Result.X = temp_x * COS(angle) - temp_y * SIN(angle)
Result.Y = temp_x * SIN(angle) + temp_y * COS(angle)
Result.X = Result.X + pivot.X
Result.Y = Result.Y + pivot.Y
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Calculates the corner vertices for a given box
SUB GDK_GetBoxCorners (Box AS GDK_Box, Corners() AS GDK_Vector)
DIM HalfW AS SINGLE, HalfH AS SINGLE
DIM UnrotatedCorner AS GDK_Vector
DIM Center AS GDK_Vector

HalfW = Box.WidthHeight.X / 2
HalfH = Box.WidthHeight.Y / 2
Center = Box.Position

UnrotatedCorner.X = Center.X - HalfW
UnrotatedCorner.Y = Center.Y - HalfH
CALL GDK_RotatePoint(Corners(0), UnrotatedCorner, Center, Box.Rotation)

UnrotatedCorner.X = Center.X + HalfW
UnrotatedCorner.Y = Center.Y - HalfH
CALL GDK_RotatePoint(Corners(1), UnrotatedCorner, Center, Box.Rotation)

UnrotatedCorner.X = Center.X + HalfW
UnrotatedCorner.Y = Center.Y + HalfH
CALL GDK_RotatePoint(Corners(2), UnrotatedCorner, Center, Box.Rotation)

UnrotatedCorner.X = Center.X - HalfW
UnrotatedCorner.Y = Center.Y + HalfH
CALL GDK_RotatePoint(Corners(3), UnrotatedCorner, Center, Box.Rotation)
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for overlap of projections on a given axis
FUNCTION GDK_Overlap_On_Axis (Corners1() AS GDK_Vector, Corners2() AS GDK_Vector, Axis AS GDK_Vector)
DIM Min1 AS SINGLE, Max1 AS SINGLE, Min2 AS SINGLE, Max2 AS SINGLE
DIM Projection AS SINGLE, I AS LONG

Min1 = 9999999!: Max1 = -9999999!
Min2 = 9999999!: Max2 = -9999999!

FOR I = 0 TO 3
Projection = Corners1(I).X * Axis.X + Corners1(I).Y * Axis.Y
IF Projection < Min1 THEN Min1 = Projection
IF Projection > Max1 THEN Max1 = Projection
NEXT I

FOR I = 0 TO 3
Projection = Corners2(I).X * Axis.X + Corners2(I).Y * Axis.Y
IF Projection < Min2 THEN Min2 = Projection
IF Projection > Max2 THEN Max2 = Projection
NEXT I

IF Max1 >= Min2 AND Max2 >= Min1 THEN GDK_Overlap_On_Axis = -1 ELSE GDK_Overlap_On_Axis = 0
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for intersection using SAT
FUNCTION GDK_Box_Intersect (Box1 AS GDK_Box, Box2 AS GDK_Box)
DIM Box1_Corners(3) AS GDK_Vector, Box2_Corners(3) AS GDK_Vector
DIM Axis(3) AS GDK_Vector, length AS SINGLE, I AS LONG

CALL GDK_GetBoxCorners(Box1, Box1_Corners())
CALL GDK_GetBoxCorners(Box2, Box2_Corners())

Axis(0).X = -(Box1_Corners(1).Y - Box1_Corners(0).Y): Axis(0).Y = Box1_Corners(1).X - Box1_Corners(0).X
Axis(1).X = -(Box1_Corners(0).Y - Box1_Corners(3).Y): Axis(1).Y = Box1_Corners(0).X - Box1_Corners(3).X

FOR I = 0 TO 1
length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
NEXT I

Axis(2).X = -(Box2_Corners(1).Y - Box2_Corners(0).Y): Axis(2).Y = Box2_Corners(1).X - Box2_Corners(0).X
Axis(3).X = -(Box2_Corners(0).Y - Box2_Corners(3).Y): Axis(3).Y = Box2_Corners(0).X - Box2_Corners(3).X

FOR I = 2 TO 3
length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
NEXT I

GDK_Box_Intersect = -1
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_Circle_Intersect (Circle1 AS GDK_Circle, Circle2 AS GDK_Circle)
' Calculate the squared distance between the circle centers
DIM dx AS SINGLE: dx = Circle1.Position.X - Circle2.Position.X
DIM dy AS SINGLE: dy = Circle1.Position.Y - Circle2.Position.Y
DIM distance_squared AS SINGLE: distance_squared = dx * dx + dy * dy

' Calculate the sum of the radii and square it
DIM radii_sum AS SINGLE: radii_sum = Circle1.Radius + Circle2.Radius
DIM radii_sum_squared AS SINGLE: radii_sum_squared = radii_sum * radii_sum

' If the distance squared is less than or equal to the sum of radii squared, they intersect
IF distance_squared <= radii_sum_squared THEN
GDK_Circle_Intersect = -1 ' Collision
ELSE
GDK_Circle_Intersect = 0 ' No collision
END IF
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB InitCircles
DIM I AS LONG, R AS SINGLE, S AS SINGLE
FOR I = 0 TO MAX_CIRCLES - 1
R = 5 + RND * 25 ' Radius
S = 1 + RND * 10 ' Speed factor
Circles(I).Position.X = RND * _WIDTH
Circles(I).Position.Y = RND * _HEIGHT
Circles(I).Radius = R
Circles(I).Rotation_Speed = (RND - 5) * 0.3 ' Random speed and direction
Circles(I).Velocity.X = (RND * 300) + -150
Circles(I).Velocity.Y = (RND * 300) + -150
Circles(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)
Circles(I).IsColliding = GDK_FALSE
Circles(I).FlashTimer = 0
Circles(I).Mass = _PI * Circles(I).Radius * Circles(I).Radius
NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB UpdateCircles (dt AS SINGLE)
DIM I AS LONG, J AS LONG
DIM Circle_Hit AS INTEGER

FOR I = 0 TO MAX_CIRCLES - 1
' Update position and rotation
Circles(I).Position.X = Circles(I).Position.X + Circles(I).Velocity.X * dt
Circles(I).Position.Y = Circles(I).Position.Y + Circles(I).Velocity.Y * dt
Circles(I).Rotation = Circles(I).Rotation + Circles(I).Rotation_Speed * dt

' Screen wrapping
IF Circles(I).Position.X + Circles(I).Radius < 0 THEN Circles(I).Position.X = _WIDTH + Circles(I).Radius
IF Circles(I).Position.X - Circles(I).Radius > _WIDTH THEN Circles(I).Position.X = 0 - Circles(I).Radius
IF Circles(I).Position.Y + Circles(I).Radius < 0 THEN Circles(I).Position.Y = _HEIGHT + Circles(I).Radius
IF Circles(I).Position.Y - Circles(I).Radius > _HEIGHT THEN Circles(I).Position.Y = 0 - Circles(I).Radius

' Check for collision with other circles
Circle_Hit = GDK_FALSE
FOR J = I + 1 TO MAX_CIRCLES - 1
IF GDK_Circle_Intersect(Circles(I), Circles(J)) THEN
Circle_Hit = GDK_TRUE
' Simple bounce logic (swapping velocities)
DIM tempX AS SINGLE, tempY AS SINGLE
tempX = Circles(I).Velocity.X: tempY = Circles(I).Velocity.Y
Circles(I).Velocity.X = Circles(J).Velocity.X: Circles(I).Velocity.Y = Circles(J).Velocity.Y
Circles(J).Velocity.X = tempX: Circles(J).Velocity.Y = tempY
Circles(J).FlashTimer = FLASH_TIME
END IF
NEXT J

' Manage flash timer
IF Circle_Hit THEN
Circles(I).FlashTimer = FLASH_TIME
ELSEIF Circles(I).FlashTimer > 0 THEN
Circles(I).FlashTimer = Circles(I).FlashTimer - dt
END IF
NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB DrawCircles
DIM I AS LONG, pColor AS LONG
FOR I = 0 TO MAX_CIRCLES - 1
IF Circles(I).FlashTimer > 0 THEN
pColor = _RGB32(255, 255, 255) ' White flash color
ELSE
pColor = Circles(I).pColor
END IF

' Draw the circle using QB64's CIRCLE command
CIRCLE (Circles(I).Position.X, Circles(I).Position.Y), Circles(I).Radius, pColor

' To draw a filled circle, use PAINT after drawing the outline.
PAINT (Circles(I).Position.X, Circles(I).Position.Y), pColor, pColor
NEXT I
END SUB


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

SUB DrawBoxes
DIM I AS LONG
DIM pColor AS LONG
DIM Corners(3) AS GDK_Vector ' This needs to be defined

FOR I = 0 TO MAX_BOXES - 1
' Set color, flashing white if collision timer is active
IF Boxes(I).FlashTimer > 0 THEN
pColor = _RGB32(255, 255, 255) ' White flash color
ELSE
pColor = Boxes(I).pColor
END IF

' Get the world-space coordinates of the box's four corners
CALL GDK_GetBoxCorners(Boxes(I), Corners())

' Draw the box outline by connecting the corners
LINE (Corners(0).X, Corners(0).Y)-(Corners(1).X, Corners(1).Y), pColor
LINE (Corners(1).X, Corners(1).Y)-(Corners(2).X, Corners(2).Y), pColor
LINE (Corners(2).X, Corners(2).Y)-(Corners(3).X, Corners(3).Y), pColor
LINE (Corners(3).X, Corners(3).Y)-(Corners(0).X, Corners(0).Y), pColor

NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_DistanceSquared (Vec1 AS GDK_Vector, Vec2 AS GDK_Vector)
DIM dx AS SINGLE: dx = Vec1.X - Vec2.X
DIM dy AS SINGLE: dy = Vec1.Y - Vec2.Y
GDK_DistanceSquared = dx * dx + dy * dy
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_BoxCircle_Intersect (Box AS GDK_Box, pCircle AS GDK_Circle)
DIM BoxCenter AS GDK_Vector
DIM LocalCirclePos AS GDK_Vector
DIM ClampedPos AS GDK_Vector
DIM dx AS SINGLE, dy AS SINGLE, distance_squared AS SINGLE
DIM HalfWidth AS SINGLE, HalfHeight AS SINGLE

' Calculate the box's true center in world space, accounting for the rotation offset
BoxCenter.X = Box.Position.X + Box.RotationPointOffset.X
BoxCenter.Y = Box.Position.Y + Box.RotationPointOffset.Y

' Create a copy of the circle's position to rotate
LocalCirclePos = pCircle.Position

' Rotate the circle's position into the box's local, axis-aligned space
CALL GDK_RotateVector(LocalCirclePos, BoxCenter, -Box.Rotation)

' Separate the declaration and initialization as required by QB64
HalfWidth = Box.WidthHeight.X / 2.0
HalfHeight = Box.WidthHeight.Y / 2.0

' Clamp the local circle's position to the axis-aligned box in local space.
ClampedPos.X = LocalCirclePos.X
IF LocalCirclePos.X < BoxCenter.X - HalfWidth THEN ClampedPos.X = BoxCenter.X - HalfWidth
IF LocalCirclePos.X > BoxCenter.X + HalfWidth THEN ClampedPos.X = BoxCenter.X + HalfWidth

ClampedPos.Y = LocalCirclePos.Y
IF LocalCirclePos.Y < BoxCenter.Y - HalfHeight THEN ClampedPos.Y = BoxCenter.Y - HalfHeight
IF LocalCirclePos.Y > BoxCenter.Y + HalfHeight THEN ClampedPos.Y = BoxCenter.Y + HalfHeight

' Calculate the squared distance between the clamped point and the rotated circle's center
dx = ClampedPos.X - LocalCirclePos.X
dy = ClampedPos.Y - LocalCirclePos.Y
distance_squared = dx * dx + dy * dy

' Compare with the squared radius
IF distance_squared <= pCircle.Radius * pCircle.Radius THEN
GDK_BoxCircle_Intersect = -1 ' Collision
ELSE
GDK_BoxCircle_Intersect = 0 ' No collision
END IF
END FUNCTION


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB InitBoxes
DIM I AS LONG, W AS SINGLE, H AS SINGLE, S AS SINGLE
FOR I = 0 TO MAX_BOXES - 1
' Randomize box dimensions
W = 10 + RND * 50
H = 10 + RND * 50

' Randomize box position within the screen boundaries
Boxes(I).Position.X = RND * _WIDTH
Boxes(I).Position.Y = RND * _HEIGHT

Boxes(I).WidthHeight.X = W
Boxes(I).WidthHeight.Y = H
Boxes(I).Mass = Boxes(I).WidthHeight.X * Boxes(I).WidthHeight.Y
' Randomize initial rotation and rotation speed
Boxes(I).Rotation = RND * 2 * _PI
Boxes(I).Rotation_Speed = (RND * .3) - .15 ' Rotational speed can be positive or negative

' Set the rotation point to the center of the box
Boxes(I).RotationPointOffset.X = W / 2: Boxes(I).RotationPointOffset.Y = H / 2

' Randomize velocity with a scaling factor
S = 10 + RND * 5
Boxes(I).Velocity.X = (RND * 30) + -15 ' RND * 2 - 1 gives a range from -1 to 1
Boxes(I).Velocity.Y = (RND * 30) + -15

' Randomize a color
Boxes(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)

' Reset collision and flash timer state
Boxes(I).IsColliding = GDK_FALSE
Boxes(I).FlashTimer = 0
NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_RotateVector (PointToRotate AS GDK_Vector, Center AS GDK_Vector, Angle AS SINGLE)
DIM Translated AS GDK_Vector
DIM Rotated AS GDK_Vector

' Translate point so center of rotation is at the origin
Translated.X = PointToRotate.X - Center.X
Translated.Y = PointToRotate.Y - Center.Y

' Apply the rotation
Rotated.X = Translated.X * COS(Angle) - Translated.Y * SIN(Angle)
Rotated.Y = Translated.X * SIN(Angle) + Translated.Y * COS(Angle)

' Translate back and update the original vector
PointToRotate.X = Rotated.X + Center.X
PointToRotate.Y = Rotated.Y + Center.Y
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

SUB UpdateBoxes (dt AS SINGLE)
DIM I AS LONG, J AS LONG
DIM Box_Hit AS INTEGER
DIM tempX AS SINGLE, tempY AS SINGLE
DIM minX AS SINGLE, maxX AS SINGLE, minY AS SINGLE, maxY AS SINGLE
DIM Corners(3) AS GDK_Vector ' This needs to be defined

FOR I = 0 TO MAX_BOXES - 1
' Update position and rotation
Boxes(I).Position.X = Boxes(I).Position.X + Boxes(I).Velocity.X * dt
Boxes(I).Position.Y = Boxes(I).Position.Y + Boxes(I).Velocity.Y * dt
Boxes(I).Rotation = Boxes(I).Rotation + Boxes(I).Rotation_Speed * dt

' --- Screen wrapping logic ---
' Calculate the AABB (Axis-Aligned Bounding Box) of the rotated box to find its min/max extents

CALL GDK_GetBoxCorners(Boxes(I), Corners()) ' Assuming GDK_GetBoxCorners fills the Corners array

minX = Corners(0).X: maxX = Corners(0).X
minY = Corners(0).Y: maxY = Corners(0).Y

FOR k = 1 TO 3
IF Corners(k).X < minX THEN minX = Corners(k).X
IF Corners(k).X > maxX THEN maxX = Corners(k).X
IF Corners(k).Y < minY THEN minY = Corners(k).Y
IF Corners(k).Y > maxY THEN maxY = Corners(k).Y
NEXT k

' Apply wrapping based on the AABB
IF maxX < 0 THEN Boxes(I).Position.X = _WIDTH + (Boxes(I).Position.X - minX)
IF minX > _WIDTH THEN Boxes(I).Position.X = 0 - (maxX - Boxes(I).Position.X)
IF maxY < 0 THEN Boxes(I).Position.Y = _HEIGHT + (Boxes(I).Position.Y - minY)
IF minY > _HEIGHT THEN Boxes(I).Position.Y = 0 - (maxY - Boxes(I).Position.Y)

' --- Collision handling ---
Box_Hit = GDK_FALSE
FOR J = I + 1 TO MAX_BOXES - 1
IF GDK_Box_Intersect(Boxes(I), Boxes(J)) THEN
Box_Hit = GDK_TRUE
' Simple bounce logic
tempX = Boxes(I).Velocity.X: tempY = Boxes(I).Velocity.Y
Boxes(I).Velocity.X = Boxes(J).Velocity.X: Boxes(I).Velocity.Y = Boxes(J).Velocity.Y
Boxes(J).Velocity.X = tempX: Boxes(J).Velocity.Y = tempY
Boxes(J).FlashTimer = FLASH_TIME
END IF
NEXT J

' Manage flash timer for box
IF Box_Hit THEN
Boxes(I).FlashTimer = FLASH_TIME
ELSEIF Boxes(I).FlashTimer > 0 THEN
Boxes(I).FlashTimer = Boxes(I).FlashTimer - dt
END IF
NEXT I
END SUB



'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB HandleBoxCircleCollisions (dt AS SINGLE)
DIM I AS LONG, J AS LONG, GDK_Vector(0, 0) AS GDK_Vector

DIM BoxCenter AS GDK_Vector
DIM LocalCirclePos AS GDK_Vector
DIM HalfWidth AS SINGLE, HalfHeight AS SINGLE
DIM ClampedPos AS GDK_Vector
DIM Normal AS GDK_Vector
DIM Length AS SINGLE
DIM Penetration AS SINGLE
DIM MassInverseSum AS SINGLE
DIM PosCorrection AS SINGLE
DIM RelVel AS GDK_Vector
DIM VelAlongNormal AS SINGLE
DIM Restitution AS SINGLE
DIM ImpulseScalar AS SINGLE
DIM MinSeparation AS SINGLE

' Constants for the physics simulation
Restitution = 0.9
MinSeparation = 0.01

FOR I = 0 TO MAX_CIRCLES - 1
FOR J = 0 TO MAX_BOXES - 1
IF GDK_BoxCircle_Intersect(Boxes(J), Circles(I)) THEN
Circles(I).FlashTimer = FLASH_TIME
Boxes(J).FlashTimer = FLASH_TIME

' === Collision Resolution ===

' Calculate the box's true center in world space
BoxCenter.X = Boxes(J).Position.X + Boxes(J).RotationPointOffset.X
BoxCenter.Y = Boxes(J).Position.Y + Boxes(J).RotationPointOffset.Y

' Get local space circle position
LocalCirclePos = Circles(I).Position
CALL GDK_RotateVector(LocalCirclePos, BoxCenter, -Boxes(J).Rotation)

' Get box dimensions
HalfWidth = Boxes(J).WidthHeight.X / 2.0
HalfHeight = Boxes(J).WidthHeight.Y / 2.0

' Clamp the local circle's position to the axis-aligned box
ClampedPos.X = LocalCirclePos.X
IF LocalCirclePos.X < BoxCenter.X - HalfWidth THEN ClampedPos.X = BoxCenter.X - HalfWidth
IF LocalCirclePos.X > BoxCenter.X + HalfWidth THEN ClampedPos.X = BoxCenter.X + HalfWidth

ClampedPos.Y = LocalCirclePos.Y
IF LocalCirclePos.Y < BoxCenter.Y - HalfHeight THEN ClampedPos.Y = BoxCenter.Y - HalfHeight
IF LocalCirclePos.Y > BoxCenter.Y + HalfHeight THEN ClampedPos.Y = BoxCenter.Y + HalfHeight

' Calculate the normal vector and penetration depth
Normal.X = LocalCirclePos.X - ClampedPos.X
Normal.Y = LocalCirclePos.Y - ClampedPos.Y

Length = SQR(Normal.X * Normal.X + Normal.Y * Normal.Y)

Penetration = Circles(I).Radius - Length

IF Penetration > 0 THEN
IF Length > 0 THEN
' Normalize the normal vector
Normal.X = Normal.X / Length
Normal.Y = Normal.Y / Length
ELSE
' This case occurs if the circle's center is exactly on the box's center.
' Choose a random normal to prevent a division by zero.
Normal.X = 1: Normal.Y = 0
END IF

' Rotate the normal back into world space
CALL GDK_RotateVector(Normal, GDK_Vector(0, 0), Boxes(J).Rotation)

' === Positional Correction ===
MassInverseSum = 1 / Circles(I).Mass + 1 / Boxes(J).Mass

IF MassInverseSum > 0 THEN
PosCorrection = _MAX(Penetration - MinSeparation, 0) / MassInverseSum

Circles(I).Position.X = Circles(I).Position.X + Normal.X * PosCorrection / Circles(I).Mass
Circles(I).Position.Y = Circles(I).Position.Y + Normal.Y * PosCorrection / Circles(I).Mass

Boxes(J).Position.X = Boxes(J).Position.X - Normal.X * PosCorrection / Boxes(J).Mass
Boxes(J).Position.Y = Boxes(J).Position.Y - Normal.Y * PosCorrection / Boxes(J).Mass
END IF

' === Velocity Impulse ===
RelVel.X = Circles(I).Velocity.X - Boxes(J).Velocity.X
RelVel.Y = Circles(I).Velocity.Y - Boxes(J).Velocity.Y

VelAlongNormal = RelVel.X * Normal.X + RelVel.Y * Normal.Y

IF VelAlongNormal < 0 THEN
IF MassInverseSum > 0 THEN
ImpulseScalar = -(1 + Restitution) * VelAlongNormal / MassInverseSum

Circles(I).Velocity.X = Circles(I).Velocity.X + ImpulseScalar * Normal.X / Circles(I).Mass
Circles(I).Velocity.Y = Circles(I).Velocity.Y + ImpulseScalar * Normal.Y / Circles(I).Mass

Boxes(J).Velocity.X = Boxes(J).Velocity.X - ImpulseScalar * Normal.X / Boxes(J).Mass
Boxes(J).Velocity.Y = Boxes(J).Velocity.Y - ImpulseScalar * Normal.Y / Boxes(J).Mass
END IF
END IF
END IF
END IF
NEXT J
NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////




Thats circles but if you remove the ' here (line 31) itll do not quite perfect circle on box collisions too....

Crc = GDK_TRUE
'Bxs = GDK_TRUE


John
Reply
#6
Here's polygon collisions....and now im off to bed!

Code: (Select All)
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'// Unseen GDK 3 -  Collisions Dev -Shapes\\
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
RANDOMIZE TIMER
'// REM $INCLUDE:'UnseenGDK2\GDK2.bi'
CONST GDK_SHAPE_CIRCLE = 1
CONST GDK_SHAPE_BOX = 2
CONST GDK_SHAPE_TRIANGLE = 3
CONST GDK_SHAPE_POLYGON = 4

CONST GDK_Vector_Size = 8
CONST GDK_TRUE = -1, GDK_FALSE = 0
'///////////////////////////////////// System Initialisation ////////////////////////////////////////////////
'/GDK2_System_New "UnseenGDK2\Projects\", 800, 600, GDK_FALSE, "GDK Rectangle Demo"
SCREEN _NEWIMAGE(800, 600, 32)
_SCREENMOVE 0, 0
_DELAY 1

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

CONST MAX_BOXES = 20
CONST MAX_CIRCLES = 40
CONST MAX_SHAPES = 40

CONST FLASH_TIME = 0.25
DIM SHARED Boxes(MAX_BOXES) AS GDK_Box
DIM SHARED Circles(MAX_CIRCLES) AS GDK_Circle
DIM SHARED Bxs, Crc, Ply

DIM SHARED Shapes(MAX_SHAPES) AS GDK_Shape

DIM SHARED GT#, LastGT#
GT# = TIMER(.001): LastGT# = GT#
InitCircles
InitBoxes
InitShapes
'Crc = GDK_TRUE
'Bxs = GDK_TRUE


'//////////////////////////////////////// Main Loop ////////////////////////////////////////////////////////////
DO
  _LIMIT 60
  _FPS 60

  GT# = TIMER(.001)
  dt = GT# - LastGT#
  IF Crc THEN UpdateCircles dt
  IF Bxs THEN UpdateBoxes dt
  IF Crc AND Bxs THEN HandleBoxCircleCollisions dt
  UpdateShapes dt
  HandleAllCollisions

  LINE (0, 0)-(_WIDTH, _HEIGHT), _RGBA32(0, 0, 0, 20), BF

  IF Crc THEN DrawCircles
  IF Bxs THEN DrawBoxes
  RenderShapes

  _DISPLAY
  LastGT# = GT#
LOOP UNTIL INKEY$ = CHR$(27)

SYSTEM

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'REM $INCLUDE:'UnseenGDK2\GDK2.bm'
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

TYPE GDK_Vector
  X AS SINGLE
  Y AS SINGLE
END TYPE

'The new GDK_Box type for a rotated rectangle, without pre-calculated corners
TYPE GDK_Box
  Position AS GDK_Vector
  WidthHeight AS GDK_Vector
  Rotation AS SINGLE
  RotationPointOffset AS GDK_Vector
  Rotation_Speed AS SINGLE ' New: For rotational speed
  Velocity AS GDK_Vector ' Movement vector for this box
  pColor AS LONG ' Color for drawing
  IsColliding AS INTEGER ' Flag to indicate collision state
  FlashTimer AS SINGLE ' Timer for collision flash
  Mass AS SINGLE
END TYPE

TYPE GDK_Circle
  Position AS GDK_Vector ' Position vector for the circle's center
  Radius AS SINGLE ' Radius of the circle
  Rotation AS SINGLE ' Rotational property, kept as circles can still spin
  Rotation_Speed AS SINGLE ' Rotational speed
  Velocity AS GDK_Vector ' Movement vector for this circle
  pColor AS LONG ' Color for drawing
  IsColliding AS INTEGER ' Flag to indicate collision state
  FlashTimer AS SINGLE ' Timer for collision flash
  Mass AS SINGLE
END TYPE

TYPE GDK_Shape
  ShapeType AS INTEGER
  Position AS GDK_Vector
  Rotation AS SINGLE
  Rotation_Speed AS SINGLE
  RotationPointOffset AS GDK_Vector
  Velocity AS GDK_Vector
  pColor AS LONG
  Mass AS SINGLE
  IsColliding AS INTEGER
  FlashTimer AS SINGLE

  Radius AS SINGLE

  WidthHeight AS GDK_Vector

  numpoints AS INTEGER
  MemVertices AS _MEM
END TYPE

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Draws a box using its calculated corner vertices.
SUB GDK_DrawBox (Box AS GDK_Box, bcolor AS LONG)
  DIM Corners(3) AS GDK_Vector
  CALL GDK_GetBoxCorners(Box, Corners())
  LINE (Corners(0).X, Corners(0).Y)-(Corners(1).X, Corners(1).Y), bcolor
  LINE (Corners(1).X, Corners(1).Y)-(Corners(2).X, Corners(2).Y), bcolor
  LINE (Corners(2).X, Corners(2).Y)-(Corners(3).X, Corners(3).Y), bcolor
  LINE (Corners(3).X, Corners(3).Y)-(Corners(0).X, Corners(0).Y), bcolor
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Rotates a point around a pivot point by a given angle (in radians)
SUB GDK_RotatePoint (Result AS GDK_Vector, p AS GDK_Vector, pivot AS GDK_Vector, angle AS SINGLE)
  DIM temp_x AS SINGLE, temp_y AS SINGLE
  temp_x = p.X - pivot.X
  temp_y = p.Y - pivot.Y
  Result.X = temp_x * COS(angle) - temp_y * SIN(angle)
  Result.Y = temp_x * SIN(angle) + temp_y * COS(angle)
  Result.X = Result.X + pivot.X
  Result.Y = Result.Y + pivot.Y
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
' Calculates the corner vertices for a given box
SUB GDK_GetBoxCorners (Box AS GDK_Box, Corners() AS GDK_Vector)
  DIM HalfW AS SINGLE, HalfH AS SINGLE
  DIM UnrotatedCorner AS GDK_Vector
  DIM Center AS GDK_Vector

  HalfW = Box.WidthHeight.X / 2
  HalfH = Box.WidthHeight.Y / 2
  Center = Box.Position

  UnrotatedCorner.X = Center.X - HalfW
  UnrotatedCorner.Y = Center.Y - HalfH
  CALL GDK_RotatePoint(Corners(0), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X + HalfW
  UnrotatedCorner.Y = Center.Y - HalfH
  CALL GDK_RotatePoint(Corners(1), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X + HalfW
  UnrotatedCorner.Y = Center.Y + HalfH
  CALL GDK_RotatePoint(Corners(2), UnrotatedCorner, Center, Box.Rotation)

  UnrotatedCorner.X = Center.X - HalfW
  UnrotatedCorner.Y = Center.Y + HalfH
  CALL GDK_RotatePoint(Corners(3), UnrotatedCorner, Center, Box.Rotation)
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for overlap of projections on a given axis
FUNCTION GDK_Overlap_On_Axis (Corners1() AS GDK_Vector, Corners2() AS GDK_Vector, Axis AS GDK_Vector)
  DIM Min1 AS SINGLE, Max1 AS SINGLE, Min2 AS SINGLE, Max2 AS SINGLE
  DIM Projection AS SINGLE, I AS LONG

  Min1 = 9999999!: Max1 = -9999999!
  Min2 = 9999999!: Max2 = -9999999!

  FOR I = 0 TO 3
    Projection = Corners1(I).X * Axis.X + Corners1(I).Y * Axis.Y
    IF Projection < Min1 THEN Min1 = Projection
    IF Projection > Max1 THEN Max1 = Projection
  NEXT I

  FOR I = 0 TO 3
    Projection = Corners2(I).X * Axis.X + Corners2(I).Y * Axis.Y
    IF Projection < Min2 THEN Min2 = Projection
    IF Projection > Max2 THEN Max2 = Projection
  NEXT I

  IF Max1 >= Min2 AND Max2 >= Min1 THEN GDK_Overlap_On_Axis = -1 ELSE GDK_Overlap_On_Axis = 0
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

' Checks for intersection using SAT
FUNCTION GDK_Box_Intersect (Box1 AS GDK_Box, Box2 AS GDK_Box)
  DIM Box1_Corners(3) AS GDK_Vector, Box2_Corners(3) AS GDK_Vector
  DIM Axis(3) AS GDK_Vector, length AS SINGLE, I AS LONG

  CALL GDK_GetBoxCorners(Box1, Box1_Corners())
  CALL GDK_GetBoxCorners(Box2, Box2_Corners())

  Axis(0).X = -(Box1_Corners(1).Y - Box1_Corners(0).Y): Axis(0).Y = Box1_Corners(1).X - Box1_Corners(0).X
  Axis(1).X = -(Box1_Corners(0).Y - Box1_Corners(3).Y): Axis(1).Y = Box1_Corners(0).X - Box1_Corners(3).X

  FOR I = 0 TO 1
    length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
    IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
    IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
  NEXT I

  Axis(2).X = -(Box2_Corners(1).Y - Box2_Corners(0).Y): Axis(2).Y = Box2_Corners(1).X - Box2_Corners(0).X
  Axis(3).X = -(Box2_Corners(0).Y - Box2_Corners(3).Y): Axis(3).Y = Box2_Corners(0).X - Box2_Corners(3).X

  FOR I = 2 TO 3
    length = SQR(Axis(I).X * Axis(I).X + Axis(I).Y * Axis(I).Y)
    IF length > 0 THEN Axis(I).X = Axis(I).X / length: Axis(I).Y = Axis(I).Y / length
    IF NOT GDK_Overlap_On_Axis(Box1_Corners(), Box2_Corners(), Axis(I)) THEN GDK_Box_Intersect = 0: EXIT FUNCTION
  NEXT I

  GDK_Box_Intersect = -1
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_Circle_Intersect (Circle1 AS GDK_Circle, Circle2 AS GDK_Circle)
  ' Calculate the squared distance between the circle centers
  DIM dx AS SINGLE: dx = Circle1.Position.X - Circle2.Position.X
  DIM dy AS SINGLE: dy = Circle1.Position.Y - Circle2.Position.Y
  DIM distance_squared AS SINGLE: distance_squared = dx * dx + dy * dy

  ' Calculate the sum of the radii and square it
  DIM radii_sum AS SINGLE: radii_sum = Circle1.Radius + Circle2.Radius
  DIM radii_sum_squared AS SINGLE: radii_sum_squared = radii_sum * radii_sum

  ' If the distance squared is less than or equal to the sum of radii squared, they intersect
  IF distance_squared <= radii_sum_squared THEN
    GDK_Circle_Intersect = -1 ' Collision
  ELSE
    GDK_Circle_Intersect = 0 ' No collision
  END IF
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB InitCircles
  DIM I AS LONG, R AS SINGLE, S AS SINGLE
  FOR I = 0 TO MAX_CIRCLES - 1
    R = 5 + RND * 25 ' Radius
    S = 1 + RND * 10 ' Speed factor
    Circles(I).Position.X = RND * _WIDTH
    Circles(I).Position.Y = RND * _HEIGHT
    Circles(I).Radius = R
    Circles(I).Rotation_Speed = (RND - 5) * 0.3 ' Random speed and direction
    Circles(I).Velocity.X = (RND * 300) + -150
    Circles(I).Velocity.Y = (RND * 300) + -150
    Circles(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)
    Circles(I).IsColliding = GDK_FALSE
    Circles(I).FlashTimer = 0
    Circles(I).Mass = _PI * Circles(I).Radius * Circles(I).Radius
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB UpdateCircles (dt AS SINGLE)
  DIM I AS LONG, J AS LONG
  DIM Circle_Hit AS INTEGER

  FOR I = 0 TO MAX_CIRCLES - 1
    ' Update position and rotation
    Circles(I).Position.X = Circles(I).Position.X + Circles(I).Velocity.X * dt
    Circles(I).Position.Y = Circles(I).Position.Y + Circles(I).Velocity.Y * dt
    Circles(I).Rotation = Circles(I).Rotation + Circles(I).Rotation_Speed * dt

    ' Screen wrapping
    IF Circles(I).Position.X + Circles(I).Radius < 0 THEN Circles(I).Position.X = _WIDTH + Circles(I).Radius
    IF Circles(I).Position.X - Circles(I).Radius > _WIDTH THEN Circles(I).Position.X = 0 - Circles(I).Radius
    IF Circles(I).Position.Y + Circles(I).Radius < 0 THEN Circles(I).Position.Y = _HEIGHT + Circles(I).Radius
    IF Circles(I).Position.Y - Circles(I).Radius > _HEIGHT THEN Circles(I).Position.Y = 0 - Circles(I).Radius

    ' Check for collision with other circles
    Circle_Hit = GDK_FALSE
    FOR J = I + 1 TO MAX_CIRCLES - 1
      IF GDK_Circle_Intersect(Circles(I), Circles(J)) THEN
        Circle_Hit = GDK_TRUE
        ' Simple bounce logic (swapping velocities)
        DIM tempX AS SINGLE, tempY AS SINGLE
        tempX = Circles(I).Velocity.X: tempY = Circles(I).Velocity.Y
        Circles(I).Velocity.X = Circles(J).Velocity.X: Circles(I).Velocity.Y = Circles(J).Velocity.Y
        Circles(J).Velocity.X = tempX: Circles(J).Velocity.Y = tempY
        Circles(J).FlashTimer = FLASH_TIME
      END IF
    NEXT J

    ' Manage flash timer
    IF Circle_Hit THEN
      Circles(I).FlashTimer = FLASH_TIME
    ELSEIF Circles(I).FlashTimer > 0 THEN
      Circles(I).FlashTimer = Circles(I).FlashTimer - dt
    END IF
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB DrawCircles
  DIM I AS LONG, pColor AS LONG
  FOR I = 0 TO MAX_CIRCLES - 1
    IF Circles(I).FlashTimer > 0 THEN
      pColor = _RGB32(255, 255, 255) ' White flash color
    ELSE
      pColor = Circles(I).pColor
    END IF

    ' Draw the circle using QB64's CIRCLE command
    CIRCLE (Circles(I).Position.X, Circles(I).Position.Y), Circles(I).Radius, pColor

    ' To draw a filled circle, use PAINT after drawing the outline.
    PAINT (Circles(I).Position.X, Circles(I).Position.Y), pColor, pColor
  NEXT I
END SUB


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

SUB DrawBoxes
  DIM I AS LONG
  DIM pColor AS LONG
  DIM Corners(3) AS GDK_Vector ' This needs to be defined

  FOR I = 0 TO MAX_BOXES - 1
    ' Set color, flashing white if collision timer is active
    IF Boxes(I).FlashTimer > 0 THEN
      pColor = _RGB32(255, 255, 255) ' White flash color
    ELSE
      pColor = Boxes(I).pColor
    END IF

    ' Get the world-space coordinates of the box's four corners
    CALL GDK_GetBoxCorners(Boxes(I), Corners())

    ' Draw the box outline by connecting the corners
    LINE (Corners(0).X, Corners(0).Y)-(Corners(1).X, Corners(1).Y), pColor
    LINE (Corners(1).X, Corners(1).Y)-(Corners(2).X, Corners(2).Y), pColor
    LINE (Corners(2).X, Corners(2).Y)-(Corners(3).X, Corners(3).Y), pColor
    LINE (Corners(3).X, Corners(3).Y)-(Corners(0).X, Corners(0).Y), pColor

  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_DistanceSquared (Vec1 AS GDK_Vector, Vec2 AS GDK_Vector)
  DIM dx AS SINGLE: dx = Vec1.X - Vec2.X
  DIM dy AS SINGLE: dy = Vec1.Y - Vec2.Y
  GDK_DistanceSquared = dx * dx + dy * dy
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_BoxCircle_Intersect (Box AS GDK_Box, pCircle AS GDK_Circle)
  DIM BoxCenter AS GDK_Vector
  DIM LocalCirclePos AS GDK_Vector
  DIM ClampedPos AS GDK_Vector
  DIM dx AS SINGLE, dy AS SINGLE, distance_squared AS SINGLE
  DIM HalfWidth AS SINGLE, HalfHeight AS SINGLE

  ' Calculate the box's true center in world space, accounting for the rotation offset
  BoxCenter.X = Box.Position.X + Box.RotationPointOffset.X
  BoxCenter.Y = Box.Position.Y + Box.RotationPointOffset.Y

  ' Create a copy of the circle's position to rotate
  LocalCirclePos = pCircle.Position

  ' Rotate the circle's position into the box's local, axis-aligned space
  CALL GDK_RotateVector(LocalCirclePos, BoxCenter, -Box.Rotation)

  ' Separate the declaration and initialization as required by QB64
  HalfWidth = Box.WidthHeight.X / 2.0
  HalfHeight = Box.WidthHeight.Y / 2.0

  ' Clamp the local circle's position to the axis-aligned box in local space.
  ClampedPos.X = LocalCirclePos.X
  IF LocalCirclePos.X < BoxCenter.X - HalfWidth THEN ClampedPos.X = BoxCenter.X - HalfWidth
  IF LocalCirclePos.X > BoxCenter.X + HalfWidth THEN ClampedPos.X = BoxCenter.X + HalfWidth

  ClampedPos.Y = LocalCirclePos.Y
  IF LocalCirclePos.Y < BoxCenter.Y - HalfHeight THEN ClampedPos.Y = BoxCenter.Y - HalfHeight
  IF LocalCirclePos.Y > BoxCenter.Y + HalfHeight THEN ClampedPos.Y = BoxCenter.Y + HalfHeight

  ' Calculate the squared distance between the clamped point and the rotated circle's center
  dx = ClampedPos.X - LocalCirclePos.X
  dy = ClampedPos.Y - LocalCirclePos.Y
  distance_squared = dx * dx + dy * dy

  ' Compare with the squared radius
  IF distance_squared <= pCircle.Radius * pCircle.Radius THEN
    GDK_BoxCircle_Intersect = -1 ' Collision
  ELSE
    GDK_BoxCircle_Intersect = 0 ' No collision
  END IF
END FUNCTION


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB InitBoxes
  DIM I AS LONG, W AS SINGLE, H AS SINGLE, S AS SINGLE
  FOR I = 0 TO MAX_BOXES - 1
    ' Randomize box dimensions
    W = 10 + RND * 50
    H = 10 + RND * 50

    ' Randomize box position within the screen boundaries
    Boxes(I).Position.X = RND * _WIDTH
    Boxes(I).Position.Y = RND * _HEIGHT

    Boxes(I).WidthHeight.X = W
    Boxes(I).WidthHeight.Y = H
    Boxes(I).Mass = Boxes(I).WidthHeight.X * Boxes(I).WidthHeight.Y
    ' Randomize initial rotation and rotation speed
    Boxes(I).Rotation = RND * 2 * _PI
    Boxes(I).Rotation_Speed = (RND * .3) - .15 ' Rotational speed can be positive or negative

    ' Set the rotation point to the center of the box
    Boxes(I).RotationPointOffset.X = W / 2: Boxes(I).RotationPointOffset.Y = H / 2

    ' Randomize velocity with a scaling factor
    S = 10 + RND * 5
    Boxes(I).Velocity.X = (RND * 30) + -15 ' RND * 2 - 1 gives a range from -1 to 1
    Boxes(I).Velocity.Y = (RND * 30) + -15

    ' Randomize a color
    Boxes(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)

    ' Reset collision and flash timer state
    Boxes(I).IsColliding = GDK_FALSE
    Boxes(I).FlashTimer = 0
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_RotateVector (PointToRotate AS GDK_Vector, Center AS GDK_Vector, Angle AS SINGLE)
  DIM Translated AS GDK_Vector
  DIM Rotated AS GDK_Vector

  ' Translate point so center of rotation is at the origin
  Translated.X = PointToRotate.X - Center.X
  Translated.Y = PointToRotate.Y - Center.Y

  ' Apply the rotation
  Rotated.X = Translated.X * COS(Angle) - Translated.Y * SIN(Angle)
  Rotated.Y = Translated.X * SIN(Angle) + Translated.Y * COS(Angle)

  ' Translate back and update the original vector
  PointToRotate.X = Rotated.X + Center.X
  PointToRotate.Y = Rotated.Y + Center.Y
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

SUB UpdateBoxes (dt AS SINGLE)
  DIM I AS LONG, J AS LONG
  DIM Box_Hit AS INTEGER
  DIM tempX AS SINGLE, tempY AS SINGLE
  DIM minX AS SINGLE, maxX AS SINGLE, minY AS SINGLE, maxY AS SINGLE
  DIM Corners(3) AS GDK_Vector ' This needs to be defined

  FOR I = 0 TO MAX_BOXES - 1
    ' Update position and rotation
    Boxes(I).Position.X = Boxes(I).Position.X + Boxes(I).Velocity.X * dt
    Boxes(I).Position.Y = Boxes(I).Position.Y + Boxes(I).Velocity.Y * dt
    Boxes(I).Rotation = Boxes(I).Rotation + Boxes(I).Rotation_Speed * dt

    ' --- Screen wrapping logic ---
    ' Calculate the AABB (Axis-Aligned Bounding Box) of the rotated box to find its min/max extents

    CALL GDK_GetBoxCorners(Boxes(I), Corners()) ' Assuming GDK_GetBoxCorners fills the Corners array

    minX = Corners(0).X: maxX = Corners(0).X
    minY = Corners(0).Y: maxY = Corners(0).Y

    FOR k = 1 TO 3
      IF Corners(k).X < minX THEN minX = Corners(k).X
      IF Corners(k).X > maxX THEN maxX = Corners(k).X
      IF Corners(k).Y < minY THEN minY = Corners(k).Y
      IF Corners(k).Y > maxY THEN maxY = Corners(k).Y
    NEXT k

    ' Apply wrapping based on the AABB
    IF maxX < 0 THEN Boxes(I).Position.X = _WIDTH + (Boxes(I).Position.X - minX)
    IF minX > _WIDTH THEN Boxes(I).Position.X = 0 - (maxX - Boxes(I).Position.X)
    IF maxY < 0 THEN Boxes(I).Position.Y = _HEIGHT + (Boxes(I).Position.Y - minY)
    IF minY > _HEIGHT THEN Boxes(I).Position.Y = 0 - (maxY - Boxes(I).Position.Y)

    ' --- Collision handling ---
    Box_Hit = GDK_FALSE
    FOR J = I + 1 TO MAX_BOXES - 1
      IF GDK_Box_Intersect(Boxes(I), Boxes(J)) THEN
        Box_Hit = GDK_TRUE
        ' Simple bounce logic
        tempX = Boxes(I).Velocity.X: tempY = Boxes(I).Velocity.Y
        Boxes(I).Velocity.X = Boxes(J).Velocity.X: Boxes(I).Velocity.Y = Boxes(J).Velocity.Y
        Boxes(J).Velocity.X = tempX: Boxes(J).Velocity.Y = tempY
        Boxes(J).FlashTimer = FLASH_TIME
      END IF
    NEXT J

    ' Manage flash timer for box
    IF Box_Hit THEN
      Boxes(I).FlashTimer = FLASH_TIME
    ELSEIF Boxes(I).FlashTimer > 0 THEN
      Boxes(I).FlashTimer = Boxes(I).FlashTimer - dt
    END IF
  NEXT I
END SUB



'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB HandleBoxCircleCollisions (dt AS SINGLE)
  DIM I AS LONG, J AS LONG, GDK_Vector(0, 0) AS GDK_Vector

  DIM BoxCenter AS GDK_Vector
  DIM LocalCirclePos AS GDK_Vector
  DIM HalfWidth AS SINGLE, HalfHeight AS SINGLE
  DIM ClampedPos AS GDK_Vector
  DIM Normal AS GDK_Vector
  DIM Length AS SINGLE
  DIM Penetration AS SINGLE
  DIM MassInverseSum AS SINGLE
  DIM PosCorrection AS SINGLE
  DIM RelVel AS GDK_Vector
  DIM VelAlongNormal AS SINGLE
  DIM Restitution AS SINGLE
  DIM ImpulseScalar AS SINGLE
  DIM MinSeparation AS SINGLE

  ' Constants for the physics simulation
  Restitution = 0.9
  MinSeparation = 0.01

  FOR I = 0 TO MAX_CIRCLES - 1
    FOR J = 0 TO MAX_BOXES - 1
      IF GDK_BoxCircle_Intersect(Boxes(J), Circles(I)) THEN
        Circles(I).FlashTimer = FLASH_TIME
        Boxes(J).FlashTimer = FLASH_TIME

        ' === Collision Resolution ===

        ' Calculate the box's true center in world space
        BoxCenter.X = Boxes(J).Position.X + Boxes(J).RotationPointOffset.X
        BoxCenter.Y = Boxes(J).Position.Y + Boxes(J).RotationPointOffset.Y

        ' Get local space circle position
        LocalCirclePos = Circles(I).Position
        CALL GDK_RotateVector(LocalCirclePos, BoxCenter, -Boxes(J).Rotation)

        ' Get box dimensions
        HalfWidth = Boxes(J).WidthHeight.X / 2.0
        HalfHeight = Boxes(J).WidthHeight.Y / 2.0

        ' Clamp the local circle's position to the axis-aligned box
        ClampedPos.X = LocalCirclePos.X
        IF LocalCirclePos.X < BoxCenter.X - HalfWidth THEN ClampedPos.X = BoxCenter.X - HalfWidth
        IF LocalCirclePos.X > BoxCenter.X + HalfWidth THEN ClampedPos.X = BoxCenter.X + HalfWidth

        ClampedPos.Y = LocalCirclePos.Y
        IF LocalCirclePos.Y < BoxCenter.Y - HalfHeight THEN ClampedPos.Y = BoxCenter.Y - HalfHeight
        IF LocalCirclePos.Y > BoxCenter.Y + HalfHeight THEN ClampedPos.Y = BoxCenter.Y + HalfHeight

        ' Calculate the normal vector and penetration depth
        Normal.X = LocalCirclePos.X - ClampedPos.X
        Normal.Y = LocalCirclePos.Y - ClampedPos.Y

        Length = SQR(Normal.X * Normal.X + Normal.Y * Normal.Y)

        Penetration = Circles(I).Radius - Length

        IF Penetration > 0 THEN
          IF Length > 0 THEN
            ' Normalize the normal vector
            Normal.X = Normal.X / Length
            Normal.Y = Normal.Y / Length
          ELSE
            ' This case occurs if the circle's center is exactly on the box's center.
            ' Choose a random normal to prevent a division by zero.
            Normal.X = 1: Normal.Y = 0
          END IF

          ' Rotate the normal back into world space
          CALL GDK_RotateVector(Normal, GDK_Vector(0, 0), Boxes(J).Rotation)

          ' === Positional Correction ===
          MassInverseSum = 1 / Circles(I).Mass + 1 / Boxes(J).Mass

          IF MassInverseSum > 0 THEN
            PosCorrection = _MAX(Penetration - MinSeparation, 0) / MassInverseSum

            Circles(I).Position.X = Circles(I).Position.X + Normal.X * PosCorrection / Circles(I).Mass
            Circles(I).Position.Y = Circles(I).Position.Y + Normal.Y * PosCorrection / Circles(I).Mass

            Boxes(J).Position.X = Boxes(J).Position.X - Normal.X * PosCorrection / Boxes(J).Mass
            Boxes(J).Position.Y = Boxes(J).Position.Y - Normal.Y * PosCorrection / Boxes(J).Mass
          END IF

          ' === Velocity Impulse ===
          RelVel.X = Circles(I).Velocity.X - Boxes(J).Velocity.X
          RelVel.Y = Circles(I).Velocity.Y - Boxes(J).Velocity.Y

          VelAlongNormal = RelVel.X * Normal.X + RelVel.Y * Normal.Y

          IF VelAlongNormal < 0 THEN
            IF MassInverseSum > 0 THEN
              ImpulseScalar = -(1 + Restitution) * VelAlongNormal / MassInverseSum

              Circles(I).Velocity.X = Circles(I).Velocity.X + ImpulseScalar * Normal.X / Circles(I).Mass
              Circles(I).Velocity.Y = Circles(I).Velocity.Y + ImpulseScalar * Normal.Y / Circles(I).Mass

              Boxes(J).Velocity.X = Boxes(J).Velocity.X - ImpulseScalar * Normal.X / Boxes(J).Mass
              Boxes(J).Velocity.Y = Boxes(J).Velocity.Y - ImpulseScalar * Normal.Y / Boxes(J).Mass
            END IF
          END IF
        END IF
      END IF
    NEXT J
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

SUB GDK_Shape_NewPolygon (Shape AS GDK_Shape, Position AS GDK_Vector, Vertices() AS GDK_Vector, numpoints AS INTEGER, Mass AS SINGLE)
  DIM I AS INTEGER

  Shape.ShapeType = GDK_SHAPE_POLYGON
  Shape.Position = Position
  Shape.Mass = Mass

  ' Allocate and copy vertices using the MEM-based helper functions
  CALL GDK_Shape_NewPolygon_Mem(Shape, numpoints)
  FOR I = 0 TO numpoints - 1
    CALL GDK_Shape_SetVertex(Shape, I, Vertices(I))
  NEXT I

  ' Set the rotation point to the average center of the vertices
  ' You may need a more sophisticated method for complex shapes
  DIM Center AS GDK_Vector
  Center.X = 0: Center.Y = 0
  FOR I = 0 TO numpoints - 1
    Center.X = Center.X + Vertices(I).X
    Center.Y = Center.Y + Vertices(I).Y
  NEXT I
  Shape.RotationPointOffset.X = Center.X / numpoints
  Shape.RotationPointOffset.Y = Center.Y / numpoints

  ' Initialize other properties
  Shape.Velocity.X = 0: Shape.Velocity.Y = 0
  Shape.Rotation = 0
  Shape.Rotation_Speed = 0
  Shape.pColor = _RGB32(255, 255, 255)
  Shape.IsColliding = 0
  Shape.FlashTimer = 0
END SUB
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


' Helper for the memory allocation part, using a different name to avoid confusion
SUB GDK_Shape_NewPolygon_Mem (Shape AS GDK_Shape, numpoints AS INTEGER)
  Shape.MemVertices = _MEMNEW(numpoints * GDK_Vector_Size)
  Shape.numpoints = numpoints
END SUB
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


SUB GDK_Shape_SetVertex (Shape AS GDK_Shape, index AS INTEGER, Vertex AS GDK_Vector)

  _MEMPUT Shape.MemVertices, Shape.MemVertices.OFFSET + (index * GDK_Vector_Size), Vertex

END SUB
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


SUB GDK_Shape_GetVertex (Shape AS GDK_Shape, index AS INTEGER, Result AS GDK_Vector)
  ' Set the result UDT to default values
  Result.X = 0
  Result.Y = 0

  IF index >= 0 AND index < Shape.numpoints THEN
    _MEMGET Shape.MemVertices, Shape.MemVertices.OFFSET + (index * GDK_Vector_Size), Result
  END IF
END SUB
'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


SUB GDK_Shape_Free (Shape AS GDK_Shape)
  IF _MEMEXISTS(Shape.MemVertices) THEN
    _MEMFREE Shape.MemVertices
  END IF
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_Shape_NewCircle (Shape AS GDK_Shape, Position AS GDK_Vector, Radius AS SINGLE, Mass AS SINGLE)
  Shape.ShapeType = GDK_SHAPE_CIRCLE
  Shape.Position = Position
  Shape.Radius = Radius
  Shape.Mass = Mass
  ' Initialize other properties
  Shape.Velocity.X = 0: Shape.Velocity.Y = 0
  Shape.Rotation = 0
  Shape.Rotation_Speed = 0
  Shape.RotationPointOffset.X = 0: Shape.RotationPointOffset.Y = 0
  Shape.pColor = _RGB32(255, 255, 255)
  Shape.IsColliding = 0
  Shape.FlashTimer = 0
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_Shape_NewBox (Shape AS GDK_Shape, Position AS GDK_Vector, WidthHeight AS GDK_Vector, Mass AS SINGLE)
  Shape.ShapeType = GDK_SHAPE_BOX
  Shape.Position = Position
  Shape.WidthHeight = WidthHeight
  Shape.Mass = Mass
  ' Set the rotation point to the center of the box for convenience
  Shape.RotationPointOffset.X = WidthHeight.X / 2: Shape.RotationPointOffset.Y = WidthHeight.Y / 2
  ' Initialize other properties
  Shape.Velocity.X = 0: Shape.Velocity.Y = 0
  Shape.Rotation = 0
  Shape.Rotation_Speed = 0
  Shape.pColor = _RGB32(255, 255, 255)
  Shape.IsColliding = 0
  Shape.FlashTimer = 0
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
SUB GDK_Shape_Handle_Collisions (Shape1 AS GDK_Shape, Shape2 AS GDK_Shape)
  DIM CollisionDetected AS INTEGER
  DIM tempX AS SINGLE, tempY AS SINGLE

  CollisionDetected = 0

  SELECT CASE Shape1.ShapeType
    CASE GDK_SHAPE_CIRCLE
      SELECT CASE Shape2.ShapeType
        CASE GDK_SHAPE_CIRCLE
          IF GDK_Intersect_CircleCircle(Shape1, Shape2) THEN CollisionDetected = -1
        CASE GDK_SHAPE_BOX
          IF GDK_Intersect_CircleBox(Shape1, Shape2) THEN CollisionDetected = -1
        CASE GDK_SHAPE_TRIANGLE, GDK_SHAPE_POLYGON
          IF GDK_Intersect_CirclePolygon(Shape1, Shape2) THEN CollisionDetected = -1
      END SELECT
    CASE GDK_SHAPE_BOX
      SELECT CASE Shape2.ShapeType
        CASE GDK_SHAPE_CIRCLE
          IF GDK_Intersect_CircleBox(Shape2, Shape1) THEN CollisionDetected = -1
        CASE GDK_SHAPE_BOX
          IF GDK_Intersect_PolygonPolygon(Shape1, Shape2) THEN CollisionDetected = -1
        CASE GDK_SHAPE_TRIANGLE, GDK_SHAPE_POLYGON
          IF GDK_Intersect_PolygonPolygon(Shape1, Shape2) THEN CollisionDetected = -1
      END SELECT
    CASE GDK_SHAPE_TRIANGLE, GDK_SHAPE_POLYGON
      SELECT CASE Shape2.ShapeType
        CASE GDK_SHAPE_CIRCLE
          IF GDK_Intersect_CirclePolygon(Shape2, Shape1) THEN CollisionDetected = -1
        CASE GDK_SHAPE_BOX
          IF GDK_Intersect_PolygonPolygon(Shape1, Shape2) THEN CollisionDetected = -1
        CASE GDK_SHAPE_TRIANGLE, GDK_SHAPE_POLYGON
          IF GDK_Intersect_PolygonPolygon(Shape1, Shape2) THEN CollisionDetected = -1
      END SELECT
  END SELECT

  IF CollisionDetected THEN
    ' Flash to indicate collision
    Shape1.FlashTimer = FLASH_TIME
    Shape2.FlashTimer = FLASH_TIME

    ' === HACKY SIMPLE BOUNCE LOGIC ===
    ' This swaps velocity vectors, which produces a bouncy but not perfectly accurate result.
    tempX = Shape1.Velocity.X: tempY = Shape1.Velocity.Y
    Shape1.Velocity.X = Shape2.Velocity.X: Shape1.Velocity.Y = Shape2.Velocity.Y
    Shape2.Velocity.X = tempX: Shape2.Velocity.Y = tempY
  END IF
END SUB



'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


SUB HandleAllCollisions
  DIM I AS INTEGER, J AS INTEGER

  FOR I = 0 TO MAX_SHAPES - 1
    FOR J = I + 1 TO MAX_SHAPES - 1
      CALL GDK_Shape_Handle_Collisions(Shapes(I), Shapes(J))
    NEXT J
  NEXT I
END SUB

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_Intersect_CircleCircle (Shape1 AS GDK_Shape, Shape2 AS GDK_Shape)
  DIM dx AS SINGLE, dy AS SINGLE, distance_squared AS SINGLE
  dx = Shape1.Position.X - Shape2.Position.X
  dy = Shape1.Position.Y - Shape2.Position.Y
  distance_squared = dx * dx + dy * dy

  DIM radii_sum AS SINGLE
  radii_sum = Shape1.Radius + Shape2.Radius
  IF distance_squared <= radii_sum * radii_sum THEN
    GDK_Intersect_CircleCircle = -1
  ELSE
    GDK_Intersect_CircleCircle = 0
  END IF
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION GDK_Intersect_CirclePolygon (Circ AS GDK_Shape, Polygon AS GDK_Shape)
  ' Logic adapted from previous box-circle collision, but generalized for polygons.
  DIM PolyCenter AS GDK_Vector, LocalCirclePos AS GDK_Vector, ClampedPos AS GDK_Vector
  DIM Normal AS GDK_Vector, Length AS SINGLE, Penetration AS SINGLE
  DIM I AS INTEGER, EdgeStart AS GDK_Vector, EdgeEnd AS GDK_Vector, ClampedPointOnEdge AS GDK_Vector

  PolyCenter.X = Polygon.Position.X + Polygon.RotationPointOffset.X
  PolyCenter.Y = Polygon.Position.Y + Polygon.RotationPointOffset.Y

  LocalCirclePos = Circ.Position
  CALL GDK_RotateVector(LocalCirclePos, PolyCenter, -Polygon.Rotation)

  ' Loop through each edge of the polygon to find the closest point
  DIM MinDistanceSq AS SINGLE: MinDistanceSq = -1 ' Use a negative number as an initial flag
  FOR I = 0 TO Polygon.numpoints - 1
    CALL GDK_Shape_GetVertex(Polygon, I, EdgeStart)
    CALL GDK_Shape_GetVertex(Polygon, (I + 1) MOD Polygon.numpoints, EdgeEnd)

    GDK_ClosestPointOnLineSegment EdgeStart, EdgeEnd, LocalCirclePos, ClampedPointOnEdge

    distSq = GDK_DistanceSquared(ClampedPointOnEdge, LocalCirclePos)
    IF MinDistanceSq < 0 OR distSq < MinDistanceSq THEN
      MinDistanceSq = distSq
      ClampedPos = ClampedPointOnEdge
    END IF
  NEXT I

  IF MinDistanceSq < 0 THEN
    GDK_Intersect_CirclePolygon = 0: EXIT FUNCTION
  END IF

  DIM dx AS SINGLE, dy AS SINGLE
  dx = ClampedPos.X - LocalCirclePos.X
  dy = ClampedPos.Y - LocalCirclePos.Y
  distance_squared = dx * dx + dy * dy

  IF distance_squared <= Circle.Radius * Circle.Radius THEN
    GDK_Intersect_CirclePolygon = -1
  ELSE
    GDK_Intersect_CirclePolygon = 0
  END IF
END FUNCTION

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////


'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

'///////////////////////////////////////////////////////////////////////////////////////////////////////////////

FUNCTION GDK_Intersect_CircleBox (Shape1 AS GDK_Shape, Shape2 AS GDK_Shape)
  ' Ensure correct shape types for this function
  IF Shape1.ShapeType <> GDK_SHAPE_CIRCLE OR Shape2.ShapeType <> GDK_SHAPE_BOX THEN
    GDK_Intersect_CircleBox = 0
    EXIT FUNCTION
  END IF

  DIM BoxCenter AS GDK_Vector
  DIM LocalCirclePos AS GDK_Vector
  DIM ClampedPos AS GDK_Vector
  DIM dx AS SINGLE, dy AS SINGLE, distance_squared AS SINGLE
  DIM HalfWidth AS SINGLE, HalfHeight AS SINGLE

  ' Calculate the box's true center in world space, accounting for the rotation offset
  BoxCenter.X = Shape2.Position.X + Shape2.RotationPointOffset.X
  BoxCenter.Y = Shape2.Position.Y + Shape2.RotationPointOffset.Y

  ' Create a copy of the circle's position to rotate
  LocalCirclePos = Shape1.Position

  ' Rotate the circle's position into the box's local, axis-aligned space
  CALL GDK_RotateVector(LocalCirclePos, BoxCenter, -Shape2.Rotation)

  ' Separate the declaration and initialization as required by QB64
  HalfWidth = Shape2.WidthHeight.X / 2.0
  HalfHeight = Shape2.WidthHeight.Y / 2.0

  ' Clamp the local circle's position to the axis-aligned box in local space.
  ClampedPos.X = LocalCirclePos.X
  IF LocalCirclePos.X < BoxCenter.X - HalfWidth THEN ClampedPos.X = BoxCenter.X - HalfWidth
  IF LocalCirclePos.X > BoxCenter.X + HalfWidth THEN ClampedPos.X = BoxCenter.X + HalfWidth

  ClampedPos.Y = LocalCirclePos.Y
  IF LocalCirclePos.Y < BoxCenter.Y - HalfHeight THEN ClampedPos.Y = BoxCenter.Y - HalfHeight
  IF LocalCirclePos.Y > BoxCenter.Y + HalfHeight THEN ClampedPos.Y = BoxCenter.Y + HalfHeight

  ' Calculate the squared distance between the clamped point and the rotated circle's center
  dx = ClampedPos.X - LocalCirclePos.X
  dy = ClampedPos.Y - LocalCirclePos.Y
  distance_squared = dx * dx + dy * dy

  ' Compare with the squared radius
  IF distance_squared <= Shape1.Radius * Shape1.Radius THEN
    GDK_Intersect_CircleBox = -1 ' Collision
  ELSE
    GDK_Intersect_CircleBox = 0 ' No collision
  END IF
END FUNCTION

SUB GDK_ClosestPointOnLineSegment (StartPoint AS GDK_Vector, EndPoint AS GDK_Vector, TestPoint AS GDK_Vector, ResultPoint AS GDK_Vector)
  DIM Vector_AB AS GDK_Vector
  DIM Vector_AC AS GDK_Vector
  DIM ScalarProjection AS SINGLE
  DIM LengthSq AS SINGLE
  DIM T AS SINGLE

  ' Vector from start point (A) to end point (B)
  Vector_AB.X = EndPoint.X - StartPoint.X
  Vector_AB.Y = EndPoint.Y - StartPoint.Y

  ' Vector from start point (A) to test point (C)
  Vector_AC.X = TestPoint.X - StartPoint.X
  Vector_AC.Y = TestPoint.Y - StartPoint.Y

  ' Calculate the scalar projection of Vector_AC onto Vector_AB
  ScalarProjection = Vector_AC.X * Vector_AB.X + Vector_AC.Y * Vector_AB.Y

  ' Calculate the squared length of Vector_AB
  LengthSq = Vector_AB.X * Vector_AB.X + Vector_AB.Y * Vector_AB.Y

  ' Calculate the parameter 't' for the closest point on the infinite line
  IF LengthSq > 0 THEN
    T = ScalarProjection / LengthSq
  ELSE
    ' The segment has zero length (StartPoint equals EndPoint), so the closest point is StartPoint
    T = 0
  END IF

  ' Clamp 't' to the range [0, 1] to ensure the closest point is on the line segment
  IF T < 0 THEN
    T = 0
  ELSEIF T > 1 THEN
    T = 1
  END IF

  ' Calculate the final result point on the line segment
  ResultPoint.X = StartPoint.X + T * Vector_AB.X
  ResultPoint.Y = StartPoint.Y + T * Vector_AB.Y
END SUB


SUB InitShapes
  DIM I AS INTEGER, shapeType AS INTEGER
  DIM Post AS GDK_Vector, Wh AS GDK_Vector
  DIM Vertices(10) AS GDK_Vector ' Temp array for polygon vertices

  RANDOMIZE TIMER ' Seed the random number generator

  FOR I = 0 TO MAX_SHAPES - 1
    shapeType = INT(RND * 4) + 1 ' Randomly pick a shape type
    shapeType = GDK_SHAPE_POLYGON '//temp
    Post.X = RND * _WIDTH
    Post.Y = RND * _HEIGHT

    SELECT CASE shapeType
      CASE GDK_SHAPE_CIRCLE
        CALL GDK_Shape_NewCircle(Shapes(I), Post, 5 + RND * 20, 10 + RND * 40)
        Shapes(I).Velocity.X = (RND * 2 - 1) * 50
        Shapes(I).Velocity.Y = (RND * 2 - 1) * 50
      CASE GDK_SHAPE_BOX
        Wh.X = 10 + RND * 40
        Wh.Y = 10 + RND * 40
        CALL GDK_Shape_NewBox(Shapes(I), Post, Wh, 10 + RND * 40)
        Shapes(I).Rotation = RND * 2 * _PI
        Shapes(I).Rotation_Speed = (RND * 2 - 1) * 0.5
        Shapes(I).Velocity.X = (RND * 2 - 1) * 50
        Shapes(I).Velocity.Y = (RND * 2 - 1) * 50
      CASE GDK_SHAPE_TRIANGLE
        Vertices(0).X = -15: Vertices(0).Y = -15
        Vertices(1).X = 15: Vertices(1).Y = -15
        Vertices(2).X = 0: Vertices(2).Y = 15
        CALL GDK_Shape_NewPolygon(Shapes(I), Post, Vertices(), 3, 10 + RND * 20)
        Shapes(I).Rotation = RND * 2 * _PI
        Shapes(I).Rotation_Speed = (RND * 2 - 1) * 0.5
        Shapes(I).Velocity.X = (RND * 2 - 1) * 50
        Shapes(I).Velocity.Y = (RND * 2 - 1) * 50
      CASE GDK_SHAPE_POLYGON
        DIM sides AS INTEGER: sides = INT(RND * 6) + 3 ' 5 to 8 sides
        DIM angleStep AS SINGLE: angleStep = 2 * _PI / sides
        DIM radius AS SINGLE: radius = 15 + RND * 15
        FOR J = 0 TO sides - 1
          Vertices(J).X = COS(J * angleStep) * radius
          Vertices(J).Y = SIN(J * angleStep) * radius
        NEXT J
        CALL GDK_Shape_NewPolygon(Shapes(I), Post, Vertices(), sides, 10 + RND * 40)
        Shapes(I).Rotation = RND * 2 * _PI
        Shapes(I).Rotation_Speed = (RND * 2 - 1) * 0.5
        Shapes(I).Velocity.X = (RND * 2 - 1) * 50
        Shapes(I).Velocity.Y = (RND * 2 - 1) * 50
    END SELECT

    Shapes(I).pColor = _RGB32(RND * 255, RND * 255, RND * 255)
  NEXT I
END SUB


SUB UpdateShapes (dt AS SINGLE)
  DIM I AS INTEGER
  DIM RotatedCorner AS GDK_Vector
  DIM minX AS SINGLE, maxX AS SINGLE, minY AS SINGLE, maxY AS SINGLE
  DIM Corner AS GDK_Vector

  FOR I = 0 TO MAX_SHAPES - 1
    ' Update position and rotation
    Shapes(I).Position.X = Shapes(I).Position.X + Shapes(I).Velocity.X * dt
    Shapes(I).Position.Y = Shapes(I).Position.Y + Shapes(I).Velocity.Y * dt
    Shapes(I).Rotation = Shapes(I).Rotation + Shapes(I).Rotation_Speed * dt

    ' Screen wrapping based on shape type
    SELECT CASE Shapes(I).ShapeType
      CASE GDK_SHAPE_CIRCLE
        IF Shapes(I).Position.X + Shapes(I).Radius < 0 THEN Shapes(I).Position.X = _WIDTH + Shapes(I).Radius
        IF Shapes(I).Position.X - Shapes(I).Radius > _WIDTH THEN Shapes(I).Position.X = 0 - Shapes(I).Radius
        IF Shapes(I).Position.Y + Shapes(I).Radius < 0 THEN Shapes(I).Position.Y = _HEIGHT + Shapes(I).Radius
        IF Shapes(I).Position.Y - Shapes(I).Radius > _HEIGHT THEN Shapes(I).Position.Y = 0 - Shapes(I).Radius
      CASE GDK_SHAPE_BOX
        ' Simplified wrapping for box
        DIM halfW AS SINGLE: halfW = Shapes(I).WidthHeight.X / 2
        DIM halfH AS SINGLE: halfH = Shapes(I).WidthHeight.Y / 2
        IF Shapes(I).Position.X + halfW < 0 THEN Shapes(I).Position.X = _WIDTH + halfW
        IF Shapes(I).Position.X - halfW > _WIDTH THEN Shapes(I).Position.X = 0 - halfW
        IF Shapes(I).Position.Y + halfH < 0 THEN Shapes(I).Position.Y = _HEIGHT + halfH
        IF Shapes(I).Position.Y - halfH > _HEIGHT THEN Shapes(I).Position.Y = 0 - halfH
      CASE ELSE ' Polygons
        minX = 1000000!: maxX = -1000000!
        minY = 1000000!: maxY = -1000000!
        FOR J = 0 TO Shapes(I).numpoints - 1
          CALL GDK_Shape_GetVertex(Shapes(I), J, Corner)
          Corner.X = Shapes(I).Position.X + Corner.X
          Corner.Y = Shapes(I).Position.Y + Corner.Y
          IF Corner.X < minX THEN minX = Corner.X
          IF Corner.X > maxX THEN maxX = Corner.X
          IF Corner.Y < minY THEN minY = Corner.Y
          IF Corner.Y > maxY THEN maxY = Corner.Y
        NEXT J
        IF maxX < 0 THEN Shapes(I).Position.X = _WIDTH + (Shapes(I).Position.X - minX)
        IF minX > _WIDTH THEN Shapes(I).Position.X = 0 - (maxX - Shapes(I).Position.X)
        IF maxY < 0 THEN Shapes(I).Position.Y = _HEIGHT + (Shapes(I).Position.Y - minY)
        IF minY > _HEIGHT THEN Shapes(I).Position.Y = 0 - (maxY - Shapes(I).Position.Y)
    END SELECT

    ' Manage flash timer
    IF Shapes(I).FlashTimer > 0 THEN
      Shapes(I).FlashTimer = Shapes(I).FlashTimer - dt
    END IF
  NEXT I
END SUB


SUB RenderShapes
  DIM I AS INTEGER, J AS INTEGER
  DIM pColor AS LONG
  DIM prevX AS SINGLE, prevY AS SINGLE
  DIM CurrentVertex AS GDK_Vector, RotatedVertex AS GDK_Vector, GDK_Vector(0, 0) AS GDK_Vector
  DIM ShapeCenter AS GDK_Vector

  FOR I = 0 TO MAX_SHAPES - 1
    IF Shapes(I).FlashTimer > 0 THEN
      pColor = _RGB32(255, 255, 255) ' White flash color
    ELSE
      pColor = Shapes(I).pColor
    END IF

    SELECT CASE Shapes(I).ShapeType
      CASE GDK_SHAPE_CIRCLE
        CIRCLE (Shapes(I).Position.X, Shapes(I).Position.Y), Shapes(I).Radius, pColor
      CASE GDK_SHAPE_BOX
        ' Calculate corners and draw lines
        ShapeCenter.X = Shapes(I).Position.X + Shapes(I).RotationPointOffset.X
        ShapeCenter.Y = Shapes(I).Position.Y + Shapes(I).RotationPointOffset.Y
        DIM HalfW AS SINGLE: HalfW = Shapes(I).WidthHeight.X / 2
        DIM HalfH AS SINGLE: HalfH = Shapes(I).WidthHeight.Y / 2

        REDIM Corners(3) AS GDK_Vector
        Corners(0).X = ShapeCenter.X - HalfW: Corners(0).Y = ShapeCenter.Y - HalfH
        Corners(1).X = ShapeCenter.X + HalfW: Corners(1).Y = ShapeCenter.Y - HalfH
        Corners(2).X = ShapeCenter.X + HalfW: Corners(2).Y = ShapeCenter.Y + HalfH
        Corners(3).X = ShapeCenter.X - HalfW: Corners(3).Y = ShapeCenter.Y + HalfH

        FOR J = 0 TO 3
          CALL GDK_RotateVector(Corners(J), ShapeCenter, Shapes(I).Rotation)
        NEXT J

        FOR J = 0 TO 3
          LINE (Corners(J).X, Corners(J).Y)-(Corners((J + 1) MOD 4).X, Corners((J + 1) MOD 4).Y), pColor
        NEXT J
      CASE GDK_SHAPE_TRIANGLE, GDK_SHAPE_POLYGON
        IF Shapes(I).numpoints > 0 THEN
          ShapeCenter.X = Shapes(I).Position.X + Shapes(I).RotationPointOffset.X
          ShapeCenter.Y = Shapes(I).Position.Y + Shapes(I).RotationPointOffset.Y

          CALL GDK_Shape_GetVertex(Shapes(I), 0, CurrentVertex)
          RotatedVertex = CurrentVertex
          CALL GDK_RotateVector(RotatedVertex, GDK_Vector(0, 0), Shapes(I).Rotation)
          prevX = RotatedVertex.X + Shapes(I).Position.X
          prevY = RotatedVertex.Y + Shapes(I).Position.Y

          FOR J = 1 TO Shapes(I).numpoints - 1
            CALL GDK_Shape_GetVertex(Shapes(I), J, CurrentVertex)
            RotatedVertex = CurrentVertex
            CALL GDK_RotateVector(RotatedVertex, GDK_Vector(0, 0), Shapes(I).Rotation)
            LINE (prevX, prevY)-(RotatedVertex.X + Shapes(I).Position.X, RotatedVertex.Y + Shapes(I).Position.Y), pColor
            prevX = RotatedVertex.X + Shapes(I).Position.X
            prevY = RotatedVertex.Y + Shapes(I).Position.Y
          NEXT J

          ' Connect the last vertex to the first
          CALL GDK_Shape_GetVertex(Shapes(I), 0, CurrentVertex)
          RotatedVertex = CurrentVertex
          CALL GDK_RotateVector(RotatedVertex, GDK_Vector(0, 0), Shapes(I).Rotation)
          LINE (prevX, prevY)-(RotatedVertex.X + Shapes(I).Position.X, RotatedVertex.Y + Shapes(I).Position.Y), pColor
        END IF
    END SELECT
  NEXT I
END SUB

FUNCTION GDK_Intersect_PolygonPolygon (Polygon1 AS GDK_Shape, Polygon2 AS GDK_Shape)
  DIM I AS INTEGER, J AS INTEGER
  DIM Vertex1 AS GDK_Vector, Vertex2 AS GDK_Vector, EdgeNormal AS GDK_Vector, GDK_Vector(0, 0) AS GDK_Vector
  DIM Min1 AS SINGLE, Max1 AS SINGLE, Min2 AS SINGLE, Max2 AS SINGLE
  DIM Edge AS GDK_Vector

  ' Project onto axes of Polygon1
  FOR I = 0 TO Polygon1.numpoints - 1
    ' Get edge vector
    CALL GDK_Shape_GetVertex(Polygon1, I, Vertex1)
    CALL GDK_Shape_GetVertex(Polygon1, (I + 1) MOD Polygon1.numpoints, Vertex2)

    Edge.X = Vertex2.X - Vertex1.X
    Edge.Y = Vertex2.Y - Vertex1.Y

    ' Get normal to the edge
    EdgeNormal.X = -Edge.Y
    EdgeNormal.Y = Edge.X
    length = SQR(EdgeNormal.X * EdgeNormal.X + EdgeNormal.Y * EdgeNormal.Y)
    IF length > 0 THEN
      EdgeNormal.X = EdgeNormal.X / length
      EdgeNormal.Y = EdgeNormal.Y / length
    END IF

    ' Rotate normal to world space
    CALL GDK_RotateVector(EdgeNormal, GDK_Vector(0, 0), Polygon1.Rotation)

    ' Project polygons onto this axis
    CALL GDK_ProjectPolygon(Polygon1, EdgeNormal, Min1, Max1)
    CALL GDK_ProjectPolygon(Polygon2, EdgeNormal, Min2, Max2)

    ' Check for overlap
    IF NOT GDK_Overlap(Min1, Max1, Min2, Max2) THEN
      GDK_Intersect_PolygonPolygon = 0
      EXIT FUNCTION
    END IF
  NEXT I

  ' Project onto axes of Polygon2
  FOR I = 0 TO Polygon2.numpoints - 1
    ' Get edge vector
    CALL GDK_Shape_GetVertex(Polygon2, I, Vertex1)
    CALL GDK_Shape_GetVertex(Polygon2, (I + 1) MOD Polygon2.numpoints, Vertex2)

    Edge.X = Vertex2.X - Vertex1.X
    Edge.Y = Vertex2.Y - Vertex1.Y

    ' Get normal to the edge
    EdgeNormal.X = -Edge.Y
    EdgeNormal.Y = Edge.X
    length = SQR(EdgeNormal.X * EdgeNormal.X + EdgeNormal.Y * EdgeNormal.Y)
    IF length > 0 THEN
      EdgeNormal.X = EdgeNormal.X / length
      EdgeNormal.Y = EdgeNormal.Y / length
    END IF

    ' Rotate normal to world space
    CALL GDK_RotateVector(EdgeNormal, GDK_Vector(0, 0), Polygon2.Rotation)

    ' Project polygons onto this axis
    CALL GDK_ProjectPolygon(Polygon1, EdgeNormal, Min1, Max1)
    CALL GDK_ProjectPolygon(Polygon2, EdgeNormal, Min2, Max2)

    ' Check for overlap
    IF NOT GDK_Overlap(Min1, Max1, Min2, Max2) THEN
      GDK_Intersect_PolygonPolygon = 0
      EXIT FUNCTION
    END IF
  NEXT I

  ' If no separating axis found, there is a collision
  GDK_Intersect_PolygonPolygon = -1
END FUNCTION

' Helper sub to project a polygon onto an axis
SUB GDK_ProjectPolygon (Shape AS GDK_Shape, Axis AS GDK_Vector, Min AS SINGLE, Max AS SINGLE)
  DIM I AS INTEGER
  DIM Vertex AS GDK_Vector, GDK_Vector(0, 0) AS GDK_Vector
  DIM RotatedVertex AS GDK_Vector
  DIM Projection AS SINGLE

  ' Rotate first vertex to world space
  CALL GDK_Shape_GetVertex(Shape, 0, Vertex)
  RotatedVertex = Vertex
  CALL GDK_RotateVector(RotatedVertex, GDK_Vector(0, 0), Shape.Rotation)
  RotatedVertex.X = RotatedVertex.X + Shape.Position.X
  RotatedVertex.Y = RotatedVertex.Y + Shape.Position.Y

  ' Project first vertex onto axis
  Projection = RotatedVertex.X * Axis.X + RotatedVertex.Y * Axis.Y
  Min = Projection
  Max = Projection

  ' Loop through remaining vertices
  FOR I = 1 TO Shape.numpoints - 1
    CALL GDK_Shape_GetVertex(Shape, I, Vertex)
    RotatedVertex = Vertex
    CALL GDK_RotateVector(RotatedVertex, GDK_Vector(0, 0), Shape.Rotation)
    RotatedVertex.X = RotatedVertex.X + Shape.Position.X
    RotatedVertex.Y = RotatedVertex.Y + Shape.Position.Y

    ' Project vertex onto axis
    Projection = RotatedVertex.X * Axis.X + RotatedVertex.Y * Axis.Y

    ' Update min and max projections
    IF Projection < Min THEN Min = Projection
    IF Projection > Max THEN Max = Projection
  NEXT I
END SUB

' Helper function to check if two intervals overlap
FUNCTION GDK_Overlap (Min1 AS SINGLE, Max1 AS SINGLE, Min2 AS SINGLE, Max2 AS SINGLE)
  IF Min1 <= Max2 AND Min2 <= Max1 THEN
    GDK_Overlap = -1
  ELSE
    GDK_Overlap = 0
  END IF
END FUNCTION

John
Reply
#7
When it comes to collisions, I often find a simple color mask to be the best way to check for them, in many cases.

For example, you draw a spaceship on a second screen which isn't for display.  Unlike your display screen which is all colors and shadows and detail, this is just a red blob of singular color.

Then on that same hidden screen, you draw asteroids in various shades of yellow.   

Now all you have to do is a simple read of that screen and look for any pixels where red+yellow are both present, and you have collision.  What's even more impressive is that you *know* which object collided with the ship due to the amount of red/yellow blending in that single pixel.   One single pass to read the color values using POINT or _MEM, and you have pixel perfect collision for the ship with every asteroid on screen.

It's just a simple method which folks ought to keep in the back of their minds. Wink

From my personal experience, folks who are good at math and who rely on math, often jump immediately to formulas and math to solve every problem -- even if something much simpler such as just tossing red paint and yellow paint at the wall just to look for orange where they meet, can do the same thing often quicker and simpler.
Reply
#8
A quick color blending demo for collision as I indicated above is here: https://qb64phoenix.com/forum/showthread...7#pid35797
Reply
#9
Pixel perfect has never been something ive found an actual need for, I made things for it years ago using a similar method as yours but it calculated the exact points of intersection from each sprite and then only scanned those bits of the images...it was a nightmare and overkill! Nowadays I use boxes 80% of the size of the sprite, itll miss a few edge collisions maybe but it gives a good approximation of being pixel perfect without the over head. 

In an ideal world, people would use something like my SAT for boxes or as simple radius approach to do an initial collision check, then if that's true do something like yours...though as you'd now would have the exact area to check you could reduce the POINT calls to only the intersection area.

Oh and if you're saying i'm good at math! I wish! But thanks! Made me smile!  

Unseen
Reply
#10
I think the only time I've ever seen pixel-perfect collision actually required was several years back in a promotional game for the local Halloween costume shop. It was a simple little game where you started out with something resembling a random maze, with walls 100 pixels in size, and you navigate from one end to the next with a character 50 pixels in size.  You navigate from the starting point to the end point, do a few levels at this intensity and then it increases to the next game level.

As the game progresses, you start to get little beeps and warnings of "bad tiles" which you could walk into like pit traps and such that would give you a game over.  Those beeps start out really audible and noticeable, but over time they get quieter and easier to miss as the stages progress.

Every level increase, the game basically doubles the maze and halves the player size and wall spacing.   Level 2 is 50 pixel-wide walls and a hero 25 pixels in size.   Level 3 is 25 with a 12 pixel hero.  And so on until the last level when you have a single-pixel wide hero and a single-pixel wide hallway to navigate...

Those who make it to the end get some in-store coupon code for X% off and a promotional bag of candy or some such.   

The "Trick" involved in this "Treat" was that as you played, most folks would turn the volume up to hear those warning beeps and boops and whatnot, to make-up for the game getting quieter and harder...   Then, once you got to that last stage, your speakers were maxxed out and you were staring at the screen and concentrating terribly for pixel-perfect movement...  

and...

[Image: devil-satan-demon-evil-lucifer-monster-h...83-331.jpg]

Suddenly that flashed repeatedly on your screen and made the loudest, most extreme roaring noise and evil cackle imaginable at you!!

Not only did it scare the BEEP out of you, but it also scared everyone nearby when you yelped, something roared, and you fell out of your chair yelling "JESUS GOD PUCK WHAT THE HELLO WORLD!!!!"

*That* required pixel-perfect precision to get to the end of the maze, but that was with a 1-pixel hero moving across paths 1-pixel in width.  I can't imagine collision detection was much more than the 8 pixels surrounding the hero...  but it was, indeed, pixel-perfect collision.

(And an amazing promo for the store as *everybody* wanted their friends to try it out so they could scare the bejeebers out of them with it and laugh at them at the jump scream.)
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  WinAPI Mouse Demo Pete 0 183 12-20-2025, 06:40 PM
Last Post: Pete
  Hyperlink Demo in SCREEN 0 Pete 2 360 11-02-2025, 07:13 PM
Last Post: madscijr
  Space Invaders knock off to show simple collisions Unseen Machine 10 1,073 09-09-2025, 07:23 PM
Last Post: Pete
  Email Demo (from 6/6/2014) SMcNeill 0 594 11-13-2023, 06:39 AM
Last Post: SMcNeill
  Qix Demo james2464 4 1,088 11-23-2022, 07:01 PM
Last Post: johnno56

Forum Jump:


Users browsing this thread: 2 Guest(s)