Detecting GUI collision for minigame

GUI Collision.
I need a square to fire scripts when overlapped with other objects/Gui objects. I have achieved this to some extent, but it lacks the ability to rotate. I have tried to find a way for the math to work countless times and even asked my math teacher for advice. We have nothing.

Here is the normal code I have working so far.

if plr.Position.X.Scale + plr.Size.X.Scale > script.Parent.Position.X.Scale
and plr.Position.X.Scale < script.Parent.Position.X.Scale + script.Parent.Size.X.Scale
and plr.Position.Y.Scale + plr.Size.Y.Scale > script.Parent.Position.Y.Scale
and plr.Position.Y.Scale < script.Parent.Position.Y.Scale + script.Parent.Size.Y.Scale then

That only allows the Entity/Object meant to damage the player stay at rotation 0. How would I for instance get it to work with rotation at 45 degrees.

Like this for instance. Red is the imaginary collision that will hurt the player. Green is the player collision. Blue is the physical, visible object.

I’m trying to get the red line to match the blue line. I know that you can use y = mx + b, for it to an extent, but that only gets the left side of the object. I know there is a way of getting a line to rotate about a circle and that way I could set the diameter to the object width and set two lines on either side, but then I have the issue with it not having a proper top or bottom.

If anyone is incredibly good with math that would help a ton! Thanks!

5 Likes

this is more of an ingenuity thing than a math thing. this could help

1 Like

Our very own EgoMoose has a video on this. https://youtu.be/IELWpIGtjRg

1 Like

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

Also, I don’t want to bother but is it possible to do this for a circular object? Like to get if it’s passed the circumference. Somethings in my game arent so straight.

1 Like

Yes, you can also do this for a circular object. Here is the code that accomplishes this:

local function createFrameCircleCollider(frame)
	-- get absolute size+position of the circle
	local absolutePosition = frame.AbsolutePosition
	local absoluteSize = frame.AbsoluteSize
	
	-- get the radius and center positions of the circle
	local radius = absoluteSize.X/2
	local center_x = absolutePosition.X + radius
	local center_y = absolutePosition.Y + radius
	
	-- return a function that checks if a non-rotated rectangle intersects with the circle
	return function(guiObject)
		local pos = guiObject.AbsolutePosition
		local size = guiObject.AbsoluteSize
		
		local width = size.X
		local height = size.Y
		local pos_x = pos.X
		local pos_y = pos.Y
		
		local x = center_x < pos_x and pos_x or center_x > pos_x + width and pos_x + width or center_x
		local y = center_y < pos_y and pos_y or center_y > pos_y + height and pos_y + height or center_y
		
		local dist_x = center_x - x
		local dist_y = center_y - y
		local dist = math.sqrt(dist_x^2 + dist_y^2)
		
		return dist <= radius
	end
end

Demo place: 2d collision for o - Roblox

You use this in the same way that you would use the other code. You might consider wrapping this aswell, since you need to account for the same changes as the other code.

3 Likes