What would be the best way to figure out if a Player is inside an area(Shape of A Square)?

What would be the best way to figure out if a player is inside a area (the shape of a square)?

  1. Region3?
  2. Magnitude?
  3. Or Something else?

Scenario Desired: As soon as the player enters an area in the shape of a square (staying) in it, a GUI pops up on the screen in the duration of the stay.

I think shape.touched:connect(function())
Is the best way

1 Like

I would suggest workspace:GetPartBoundsInBox() which returns a table of parts in a specific region. The difference between it and Region3 is that it also takes CFrame and overlap params.

I was gonna provide an example but I just wasted the past 40 minutes trying to come up with an efficient way of it removing the gui without performing nested nested for loops

1 Like

You would be using spatial queries to check if a player’s humanoidrootpart is in bounds of a square. There’s a module for this on the devforum that does it very well.

2 Likes

As @TenBlocke said, there is a module on the marketplace that creates events which you can bind your UI displaying/hiding functions to. (zone.playerEntered, zone.playerExited, etc.) It’s quite popular and looks pretty straight forward to use.

local ZoneModule = require(6245329519)
local Zone = ZoneModule.new(game.Workspace:WaitForChild("Zone"))
local MasterUI = script:WaitForChild("ZoneDisplay") --A UI with a unique name.

Zone.playerEntered:Connect(function(Player)
	if Player.PlayerGui:FindFirstChild(MasterUI.Name) then
		Player.PlayerGui:FindFirstChild(MasterUI.Name).Enabled = true
	else
		MasterUI:clone().Parent = Player.PlayerGui
	end
end)

Zone.playerExited:Connect(function(Player)
	if Player.PlayerGui:FindFirstChild(MasterUI.Name) then
		Player.PlayerGui:FindFirstChild(MasterUI.Name).Enabled = false
	end
end)

I’m not sure how frequent/accurately that module runs but if you don’t want to rely on a module, I think the best process would be to use the BasePart.Touched event and collect the touching parts from the zone by using BasePart:GetTouchingParts(), workspace:GetPartsInPart(), or workspace:GetPartBoundsInBox().

After you’ve determined which parts are player parts, you can give the players a new set of UI or enable it if they already have one.

You will have to contain it in a loop with short delays for the best frequency. If you bind it directly to a Touched event or run TouchEnded checks, it may run too often, greatly degrading performance.

Here’s an example. This uses a few debounces. One for processing the initial Touched event and another for a change in part count.

local TouchDetector = game:GetService("Workspace"):WaitForChild("Zone")
TouchDetector.Transparency = 1 --Assuming you don't want the player to see it, we'll make it invisible.
TouchDetector.Anchored = true --Anchored so it doesn't fall through the map.
TouchDetector.CanCollide = false --We want to make sure players don't run into an invisible wall.
TouchDetector.CanTouch = true --Needs to be set to true so we can bind a function to the Touched event.
TouchDetector.CanQuery = true --Needs to be set to true for spatial/touching queries.

local MasterUI = script:WaitForChild("ZoneDisplay") --Needs to have a unique name, cannot be shared with other UI names since we will use FindFirstChild.
local DefaultTouchingParts = TouchDetector:GetTouchingParts() --Gather the initial touching parts. We will reference this as the default touching parts and filter against it so we aren't doing unnecessary calculations on non-player parts.

local RefreshDelay = 0.1 --Set a delay variable for the loop to reduce server script activity to greatly reduce performance issues. One tenth of a second should be fine for the delay. The higher the number, the less impact on performance.

local TouchProcessing = false --This will be the variable for our debounce.
local CurrentDisplayingUI = {} --This will contain references to all the player's zone UI.

function FetchPlayer(Part)
	if Part.Parent:FindFirstChild("Humanoid") then --If the part's model has a humanoid (NPC or Player)
		if Part.Parent.Humanoid.Health > 0 or Part.Parent.Humanoid.MaxHealth == 0 then --Make sure the humanoid is alive.
			local Player = game:GetService("Players"):GetPlayerFromCharacter(Part.Parent) --Make sure it's a player and not an NPC.
			return Player
		end
	end
	return nil
end

TouchDetector.Touched:connect(function() --Bind the following function to the Touched event:
	if TouchProcessing == false then --If the debounce hasn't been triggered yet,
		TouchProcessing = true --trigger the debounce so additional touch events don't create a bunch of the same loop.
		local PreviousTouchingPlayers = {} --We'll make a table to reference the previous touching players so we can compare against the current ones to detect who has EXITED the zone.
		local NumberOfPreviousTouchingParts  --A reference just to keep track of the previous quantity of touched parts.
		while task.wait(RefreshDelay) do --We will repeat this loop as long as there are parts touching the zone with a delay we set earlier.
			local TOTALCurrentTouchingParts = TouchDetector:GetTouchingParts() --A raw list of ALL touching parts.
			if NumberOfPreviousTouchingParts ~= #TOTALCurrentTouchingParts then --If there is a difference/change in the amount of parts from when it was last processed/someone entered or left the zone. (If someone is standing still in the zone or there isn't a change, it won't process, which is good for performance.)
				NumberOfPreviousTouchingParts = #TOTALCurrentTouchingParts --Reset the previous amount to the current amount.
				local CurrentTouchingParts = {} --A table reference to contain the current touching parts. (FILTERED)
				local CurrentTouchingPlayers = {} --A table reference to contain our current touching players.
				
				for i,v in pairs (TOTALCurrentTouchingParts) do --We will run a loop to filter out unnecessary parts from the raw parts list (models, terrain, decorations, etc.)
					if not table.find(DefaultTouchingParts, v) then --If its a non-default part, then it must be a NEW touching part so we will
						table.insert(CurrentTouchingParts, v) --add it to the CurrentTouchingParts list.
					end
				end
				
				for i,v in pairs(CurrentTouchingParts) do --For every touching part (filtered),
					local PartsPlayer = FetchPlayer(v) --check if it's a player part.
					if PartsPlayer then  --If it's a player part,
						if not table.find(CurrentTouchingPlayers, PartsPlayer) then --(Duplicate check)
							table.insert(CurrentTouchingPlayers,PartsPlayer) --add it to the CurrentTouchingPlayers list
						end
						if not table.find(PreviousTouchingPlayers, PartsPlayer) then --(Duplicate check)
							table.insert(PreviousTouchingPlayers,PartsPlayer) --and update the PreviousTouchingPlayers list.
						end
					else --If it's a non-player part,
						table.insert(DefaultTouchingParts,v) --add it to the default parts list.
					end
				end
				
				for i,v in pairs(PreviousTouchingPlayers) do --For every player that was given a UI, 
					if not table.find(CurrentTouchingPlayers, v) then --check if they are in the current zone list. If they are not in the list, they are not in the zone. And if they are not in the zone, 
						PreviousTouchingPlayers[i] = nil --remove their table reference.
						if v.Parent ~= game:GetService("Players") then continue end --Make sure they are still in-game.
						local Character = v.Character if not Character then continue end --Make sure they have a character.
						local Human = Character:FindFirstChild("Humanoid") if not Human then continue end --Make sure they have a humanoid before checking the humanoid's reference.
						if CurrentDisplayingUI[Human] then --If there is a reference to their UI from their humanoid,
							CurrentDisplayingUI[Human].Enabled = false --disable it.
						end
					end
				end
				
				for i,v in pairs(CurrentTouchingPlayers) do --For every player currently in the zone,
					if v.Parent ~= game:GetService("Players") then continue end --make sure they are still in-game,
					local Character = v.Character if not Character then continue end --make sure they have a character,
					local Human = Character:FindFirstChild("Humanoid") if not Human then continue end --and make sure they have a humanoid.
					if not v.PlayerGui:FindFirstChild(MasterUI.Name) then --If they do not have a UI, 
						local PlayersUI = MasterUI:clone() --create one,
						PlayersUI.Parent = v.PlayerGui --move it to the player's screen,
						PlayersUI.Enabled = true --and enable it.
						CurrentDisplayingUI[Human] = PlayersUI --Set a reference to their UI using their living humanoid (so it can be disabled using the loop above)
					else --If the player already has a UI,
						CurrentDisplayingUI[Human] = v.PlayerGui:FindFirstChild(MasterUI.Name) --make sure the reference is set correctly 
						if v.PlayerGui:FindFirstChild(MasterUI.Name).Enabled == false then --and if the UI is disabled,
							v.PlayerGui:FindFirstChild(MasterUI.Name).Enabled = true --enable it.
						end
					end
				end
				
				if #CurrentTouchingPlayers == 0 then TouchProcessing = false break end --Reset the debounce and exit the loop when no players are touching the zone.
				
			end
		end
	end
end)

I believe :GetTouchingParts() will be better since it only uses a TouchTransmitter/TouchInterest vs something impactful like :GetPartsInPart() which runs full geometry/spatial queries (less ideal for performance when used inside loops like in this example.)

If you have advanced collisions, MeshPart zones, or want to use filters, you should go ahead an use the :GetPartsInPart() function.

Alternatively, you can use :GetPartBoundsInBox() function but you have to supply the Zone.CFrame and Zone.Size. Since you’ll be taking both of those properties from the zone part, I don’t see why you wouldn’t just utilize one of the other functions that uses the part directly.

A much simpler approach would be to hide/display it when touch connections trigger from a character part.

game:GetService("Players").PlayerAdded:connect(function(Player)
	Player.CharacterAdded:connect(function(Char)
		local Head = Char:WaitForChild("Head")
		local MasterUI = Player.PlayerGui:WaitForChild("ZoneDisplay")
		Head.Touched:connect(function(Hit)
			if Hit == Zone then
				MasterUI.Enabled = true
			end
		end)
		Head.TouchEnded:connect(function(Hit)
			if Hit == Zone then
				MasterUI.Enabled = false
			end
		end)
	end)
end)

I think the .Touched and .TouchE ded functions are the best in that scenario.