Rotated 2D rectangle and circle collision

I’m having some trouble detecting a collision between a circle and a rotated rectangular frame. I looked at previous topics on this issue, such as this one, but none of them take into account the rotation of the rectangle frame and they assume that it is a non-rotated frame.

I am trying to get it so that when the circle intersects the rotated rectangular frame, the frame turns green. If the circle is not intersecting the rotated frame, then the frame will stay red.

This is what I have so far. When I manually set the radius of it to something like 500, it works but the collision is still off by quite a bit.

local c = script.Parent.Circle
local r = script.Parent.Frame

local function getCollision()
	local rad = c.Size.Y.Offset/2
	local cx = c.AbsolutePosition.X;      
	local cy = c.AbsolutePosition.Y;   

	local rx = r.Position.X.Offset;    
	local ry = r.Position.Y.Offset;
	local rw = r.Size.X.Offset;    
	local rh = r.Size.Y.Offset;

	local testX = cx;
	local testY = cy;

	if (cx < rx) then
		testX = rx; 
	elseif (cx > rx+(rw)) then
		testX = rx+(rw);
	end

	if (cy < ry) then        
		testY = ry;
	elseif (cy > ry+(rh)) then
		testY = ry+(rh);
	end

	local distX = cx-testX;
	local distY = cy-testY;
	local distance = math.sqrt((distX*distX) + (distY*distY));

	if (distance <= rad) then
		r.BackgroundColor3 = BrickColor.Green().Color
	else
		r.BackgroundColor3 = BrickColor.Red().Color
	end
end

game:GetService("RunService").RenderStepped:Connect(function()
	getCollision()
end)

-- Moves the circle (for collision testing purposes)

spawn(function()
	while true do
		for i = 0, 1, 0.01 do
			c.Position = UDim2.new(i, 0, 0.6, 0)
			wait()
		end
		
		wait(1)
		
		for i = 1, 0, -0.01 do
			c.Position = UDim2.new(i, 0, 0.6, 0)
			wait()
		end
	end
end)

This function finds the closest point of each edge to the center of the circle, and calculates its squared distance to the edge. u and v are vectors. their directions are the directions of the edges and their lengths are half the lengths of the edges. a is the center of an edge, and q is the closest point. If the distance between an edge and the circle is smaller than the radius of the circle, then they intersect and the function returns true. Otherwise, it checks on which side of the edge the circle center is using t. If t is positive, then the circle center is on the outer side of the edge and therefore it can’t be completely inside the rectangle, but it can still intersect another edge. If no intersections were found but the circle was on the inner side of every edge, then the whole circle is inside the rectangle and the function returns true. Here’s how I got the formula for the vector multipliers s and t using a calculator program.

local function getVecMulsForClosestPointOnLine(ax, ay, ux, uy, vx, vy, px, py)
	local divisor = ux * vy - uy * vx
	local s, t = (-ax * vy + ay * vx + px * vy - py * vx) / divisor, (ax * uy - ay * ux - px * uy + py * ux) / divisor
	return s, t
end

local function isCircleCollidingWithFrame(circle, frame)
	local circlePos = circle.AbsolutePosition
	local radius = circle.AbsoluteSize.X / 2
	local cx, cy = circlePos.X + radius, circlePos.Y + radius
	local squaredRadius = radius * radius
	
	local framePixSize = frame.AbsoluteSize
	local halfXSize, halfYSize = framePixSize.X / 2, framePixSize.Y / 2
	
	local radRotation = math.rad(frame.Rotation)
	local rotSin, rotCos = math.sin(radRotation), math.cos(radRotation)
	
	local ux, uy = rotCos * halfXSize, rotSin * halfXSize
	local vx, vy = -rotSin * halfYSize, rotCos * halfYSize
	
	local framePos = frame.AbsolutePosition
	local frameCenterX, frameCenterY = framePos.X + halfXSize, framePos.Y + halfYSize
	
	local isInside = true
	for xm = -1, 1, 2 do
		local ax, ay = frameCenterX + xm * ux, frameCenterY + xm * uy
		local s, t = getVecMulsForClosestPointOnLine(ax, ay, vx, vy, xm * ux, xm * uy, cx, cy)
		s = math.clamp(s, -1, 1)
		local qx, qy = ax + vx * s, ay + vy * s
		if (qx - cx) * (qx - cx) + (qy - cy) * (qy - cy) < squaredRadius then
			-- intersects the edge
			return true
		end
		if t > 0 then
			isInside = false
		end
	end
	for ym = -1, 1, 2 do
		local ax, ay = frameCenterX + ym * vx, frameCenterY + ym * vy
		local s, t = getVecMulsForClosestPointOnLine(ax, ay, ux, uy, ym * vx, ym * vy, cx, cy)
		s = math.clamp(s, -1, 1)
		local qx, qy = ax + ux * s, ay + uy * s
		if (qx - cx) * (qx - cx) + (qy - cy) * (qy - cy) < squaredRadius then
			-- intersects the edge
			return true
		end
		if t > 0 then
			isInside = false
		end
	end
	-- If isInside is true after checking each edge, then the circle is completely inside but doesn't intersect any edges. If it's false, then the circle does not collide with the frame.
	return isInside
end
3 Likes

Thanks! It works great. I didn’t think about doing it that way.