Great asset!
I have tried SimpleZone and ZonePlus., but both had issues where I ended up not using those assets. Found this gem and have implemented in for my Arena zone. Works great even with multiple connections to the same part as well (I have an edge case).
Thanks for using it! Its currently client sided, would it be better if I also a version thats server sided?
It definitely needs to be server sided or at least allow it. I passed your module over to AI with specific instructions and it has now allowed me to perform my check via the Server. Works great.
Not saying this is the best way, but its working and not seeing any issues yet (memory leak, etc.)
AI Altered Version:
--// Services
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
--// Module
local RegionManager = {}
RegionManager.__index = RegionManager
--// Variables
local Configuration = script.Configuration
local FrameCounter = Configuration.CHECK_INTERVAL_FRAMES.Value
--// Internal Storage
local TrackedRegions = {}
local HeartbeatConnection
--// Private: Check if HRP is inside
local function IsInside(Region, HRP)
local InRegion = Region.Box.CFrame:PointToObjectSpace(HRP.Position)
local HalfSize = Region.Box.Size / 2
return math.abs(InRegion.X) <= HalfSize.X
and math.abs(InRegion.Y) <= HalfSize.Y
and math.abs(InRegion.Z) <= HalfSize.Z
end
--// Ensure loop
local function EnsureHeartbeat()
if HeartbeatConnection then return end
HeartbeatConnection = RunService.Heartbeat:Connect(function()
FrameCounter += 1
if FrameCounter % 2 ~= 0 then
return
end
for _, Region in pairs(TrackedRegions) do
Region.PlayersInside = Region.PlayersInside or {}
for _, player in ipairs(Players:GetPlayers()) do
local character = player.Character
local hrp = character and character:FindFirstChild("HumanoidRootPart")
if hrp then
local inside = IsInside(Region, hrp)
if inside and not Region.PlayersInside[player] then
Region.PlayersInside[player] = true
if Region.Callbacks.CallbackEnter then
task.spawn(Region.Callbacks.CallbackEnter, player)
end
elseif not inside and Region.PlayersInside[player] then
Region.PlayersInside[player] = nil
if Region.Callbacks.CallbackExit then
task.spawn(Region.Callbacks.CallbackExit, player)
end
end
else
Region.PlayersInside[player] = nil
end
end
end
end)
end
--// RegionManager
function RegionManager.New(BoxToTrack: BasePart, Callbacks: { CallbackEnter: (Player) -> (), CallbackExit: (Player) -> () })
assert(BoxToTrack and BoxToTrack:IsA("BasePart"), "BoxToTrack must be a valid BasePart")
assert(typeof(Callbacks) == "table", "Callbacks must be a table")
local Region = {
Box = BoxToTrack,
Callbacks = Callbacks,
PlayersInside = {},
}
table.insert(TrackedRegions, Region)
EnsureHeartbeat()
return Region
end
--// Get players inside a region
function RegionManager.GetPlayersInside(Region)
local list = {}
for player in pairs(Region.PlayersInside or {}) do
table.insert(list, player)
end
return list
end
--// Removal
function RegionManager.Remove(RegionObject)
for Index, Region in ipairs(TrackedRegions) do
if Region == RegionObject then
table.remove(TrackedRegions, Index)
break
end
end
if #TrackedRegions == 0 and HeartbeatConnection then
HeartbeatConnection:Disconnect()
HeartbeatConnection = nil
end
end
return RegionManager
2 Likes
--// Services
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
--// References
local RegionManager = {}
RegionManager.__index = RegionManager
--// Variables
local Configuration = script.Configuration
local FrameCounter = Configuration.CHECK_INTERVAL_FRAMES.Value
--// Internal Storage
local TrackedRegions = {}
local HeartbeatConnection
--// Private: Check if a player is inside a region
local function IsInside(Region, HRP)
local InRegion = Region.Box.CFrame:PointToObjectSpace(HRP.Position)
local HalfSize = Region.Box.Size / 2
local IsInside = math.abs(InRegion.X) <= HalfSize.X
and math.abs(InRegion.Y) <= HalfSize.Y
and math.abs(InRegion.Z) <= HalfSize.Z
return IsInside
end
--// Private: Start the heartbeat loop once
local function EnsureHeartbeat()
if HeartbeatConnection then
return
end
HeartbeatConnection = RunService.Heartbeat:Connect(function()
FrameCounter += 1
if FrameCounter % 2 ~= 0 then
return
end
-- Check all players
for _, Player in pairs(Players:GetPlayers()) do
local Character = Player.Character
if not Character then
continue
end
local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
if not HumanoidRootPart then
continue
end
-- Check each region
for _, Region in pairs(TrackedRegions) do
local IsPlayerInside = IsInside(Region, HumanoidRootPart)
local WasPlayerInside = Region.PlayersInside[Player]
if IsPlayerInside and not WasPlayerInside then
-- Player entered
Region.PlayersInside[Player] = true
if Region.Callbacks.CallbackEnter then
task.spawn(Region.Callbacks.CallbackEnter, Player)
end
elseif not IsPlayerInside and WasPlayerInside then
-- Player exited
Region.PlayersInside[Player] = false
if Region.Callbacks.CallbackExit then
task.spawn(Region.Callbacks.CallbackExit, Player)
end
end
end
end
end)
end
--// Clean up player data when they leave
local function OnPlayerRemoving(Player)
for _, Region in pairs(TrackedRegions) do
if Region.PlayersInside[Player] then
Region.PlayersInside[Player] = nil
if Region.Callbacks.CallbackExit then
task.spawn(Region.Callbacks.CallbackExit, Player)
end
end
end
end
Players.PlayerRemoving:Connect(OnPlayerRemoving)
--// RegionManager
function RegionManager.New(BoxToTrack: BasePart, Callbacks: { CallbackEnter: (Player) -> (), CallbackExit: (Player) -> () }, CallbackExitFirst: boolean?)
assert(BoxToTrack and BoxToTrack:IsA("BasePart"), "BoxToTrack must be a valid BasePart")
assert(typeof(Callbacks) == "table", "Callbacks must be a table")
local Region = {
Box = BoxToTrack,
Callbacks = Callbacks,
PlayersInside = {}
}
table.insert(TrackedRegions, Region)
EnsureHeartbeat()
if CallbackExitFirst and Region.Callbacks.CallbackExit then
for _, Player in pairs(Players:GetPlayers()) do
Region.PlayersInside[Player] = false
task.spawn(Region.Callbacks.CallbackExit, Player)
end
end
return Region
end
--// Get players currently inside a region
function RegionManager.GetPlayersInside(RegionObject)
local PlayersInside = {}
for Player, IsInside in pairs(RegionObject.PlayersInside) do
if IsInside then
table.insert(PlayersInside, Player)
end
end
return PlayersInside
end
--// Check if a specific player is inside a region
function RegionManager.IsPlayerInside(RegionObject, Player: Player)
return RegionObject.PlayersInside[Player] == true
end
--// Removal
function RegionManager.Remove(RegionObject)
for Index, Region in pairs(TrackedRegions) do
if Region == RegionObject then
table.remove(TrackedRegions, Index)
break
end
end
-- Stop the heartbeat if no regions left
if #TrackedRegions == 0 and HeartbeatConnection then
HeartbeatConnection:Disconnect()
HeartbeatConnection = nil
end
end
return RegionManager
I’ve already made a server sided version, thought I’d share it ontop of that, yours seem to work great too!
1 Like
Edit: Investigating my main Minigame script currently. I will add more details if RegionManager is causing performance issues.
1 Like
Let me know if it is, I never benchmark tested it for long enough
Is it causing any issues?
////////
Based on recent tests, RegionManager does not look to be the culprit and I am moving on to building newer games. So, performance looked good to me.
Yessirrrr!!! I keep forgetting to reply
updated the region manager a bit