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)