Detecting GUI collision for minigame

So I have gone ahead and made a method which accomplishes this for you:

local function createFrameCollider(frame)
	-- get info about frame
	local frame_absoluteSize = frame.AbsoluteSize
	local frame_absolutePosition = frame.AbsolutePosition
	local frame_rotation = frame.Rotation
	
	local frame_height = frame_absoluteSize.Y
	local frame_width = frame_absoluteSize.X
	local frame_area = frame_width*frame_height
	
	local frame_center = frame_absolutePosition + frame_absoluteSize/2
	
	-- 1. roblox rotation is backwards from mathematical rotation :: negate it
	-- 2. modulus rotation by 180 to limit it to being 1 -> 180
	local frame_theta = (-frame_rotation)%180
	
	-- if rotation is negative, determine where it is by subtracting the absolute value of it from 180 (we're working with rectangles)
	-- now we have a rotation between 0 and 179.999...
	frame_theta = frame_theta < 0 and 180 - math.abs(frame_theta) or frame_theta
	
	-- if rotation >= 90, make the height the width and the width the height, then subtract 90 from the rotation
	-- now we have a rotation between 0 and 89.999..., which makes this very easy
	if frame_theta >= 90 then
		frame_height = frame_width
		frame_width = frame_absoluteSize.Y
		frame_theta = frame_theta - 90
	end
	
	-- determine the shifts we need with the frame to go from the center to an edge
	local frame_theta_rad = math.rad(frame_theta)
	local shift_y = math.sin(frame_theta_rad)*frame_width/2
	local shift_x = math.cos(frame_theta_rad)*frame_width/2
	
	-- determine the offsets we need with the frame to go from an edge to a corner
	local offset_y = math.cos(frame_theta_rad)*frame_height/2
	local offset_x = math.sin(frame_theta_rad)*frame_height/2
	
	-- get edges
	local edge_rightX = frame_center.X - shift_x
	local edge_rightY = frame_center.Y + shift_y
	local edge_leftX = frame_center.X + shift_x
	local edge_leftY = frame_center.Y - shift_y
	
	-- get corners
	local a = Vector2.new(edge_leftX - offset_x, edge_leftY - offset_y)
	local b = Vector2.new(edge_rightX - offset_x, edge_rightY - offset_y)
	local c = Vector2.new(edge_rightX + offset_x, edge_rightY + offset_y)
	local d = Vector2.new(edge_leftX + offset_x, edge_leftY + offset_y)
	local corners = {a,b,c,d}
	
	-- return function which determines if a non-rotated guiObject is intersecting this rotated guiObject
	-- uses SAT
	return function(guiObject)
		local obj_pos = guiObject.AbsolutePosition
		local obj_size = guiObject.AbsoluteSize
		local size_x = obj_size.X
		local size_y = obj_size.Y
		
		local a = obj_pos
		local b = obj_pos + Vector2.new(size_x,0)
		local c = obj_pos + Vector2.new(size_x,size_y)
		local d = obj_pos + Vector2.new(0,size_y)
		local corners2 = {a,b,c,d}
		
		for _, points in ipairs({corners, corners2}) do
			for i = 1, 4 do
				local j = i%4+1
				local p1 = points[i]
				local p2 = points[j]
				
				local normal = Vector2.new(p2.Y - p1.Y, p1.X - p2.X)
				local minA, maxA, minB, maxB
				
				for _, p in pairs(corners) do
					local projected = normal.X * p.X + normal.Y * p.Y
					minA = (minA == nil and projected) or (projected < minA and projected) or minA
					maxA = (maxA == nil and projected) or (projected > maxA and projected) or maxA
				end
				
				for _, p in pairs(corners2) do
					local projected = normal.X * p.X + normal.Y * p.Y
					minB = (minB == nil and projected) or (projected < minB and projected) or minB
					maxB = (maxB == nil and projected) or (projected > maxB and projected) or maxB
				end
				
				if maxA < minB or maxB < minA then
					return false
				end
			end
		end
		
		return true
	end
end

The createFrameCollider method returns a method which can be called to check if a given unrotated GuiObject intersects with the given frame. This accomplishes this task using SAT (see: https://youtu.be/IELWpIGtjRg as @AstroCode said). It determines the corners/vertices of the frame using sin/cos along with the absolute size/position of the frame.

So you would use this by doing something like this:

local collider = ROTATED_FRAME
local character = UNROTATED_FRAME

local detector = createFrameCollider(collider)

-- check if character collides every renderstep: make green if it does not,
-- make red if it does
game:GetService("RunService").RenderStepped:Connect(function(dt)
	local collides = collisionDetector(character)
	
	if collides then
		character.BackgroundColor3 = Color3.new(1,0.1,0.1)
	else
		character.BackgroundColor3 = Color3.new(0.2,1,0.2)
	end
end)

You can also check out this demo place: [Open Source] 2d Collision - Roblox
It is open source.

Note: every time you update the frame position you must create a new frame collider, as it uses a “cached” version of the frame to increase speed. Pretty much you need to update it any time the AbsoluteSize or AbsolutePosition of the frame changes. You can either do this manually, or wrap the method I created to do this for you automatically. The wrapped version would look something like this:

local function wrappedCollisionDetectorCreator(frame)
	local absolutePosition = frame.AbsolutePosition
	local absoluteSize = frame.AbsoluteSize
	
	local collisionDetector = createFrameCollider(frame)
	
	return function(guiObject)
		if frame.AbsolutePosition ~= absolutePosition or frame.absoluteSize ~= absoluteSize then
			absolutePosition = frame.AbsolutePosition
			absoluteSize = frame.AbsoluteSize
			collisionDetector = createFrameCollider(frame)
		end
		
		return collisionDetector(guiObject)
	end
end

Note: I have not tested this wrapper, but in theory it should work… but might need some tweaks. You would then call wrappedCollisionDetectorCreator() instead of createFrameCollider(). Naming could be better though. Edit it as you please.

Hope this helps, if you need any further explanation let me know.

16 Likes