RegionManger | Lightweight & Simpler alternative to ZonePlus

RegionManager should mainly be used for checking if a player entered a zone or a box in the physical workspace, you can attach functions to each part for said OnEnter & OnExit!

Details & Information

How Do I use RegionManager?

It’s as simple as calling the .New function in the regionmanager, you are also able to fully cleanup regions when not needed by calling the .Remove function!

local Object = path.to.part
local RegionManager = require(path.to.module)

local NewRegion = RegionManager.New(Object, {
	CallbackEnter = function()
		print("Hey! I entered")
	end,
	
	CallbackExit = function()
		print("I left!")
	end,
})

Any code in the Enter function will get executed when the client player enters that zone, the exit function is explanatory.

Why Should I use RegionManager?

RegionManager is a simple & lightweight module that dosen’t use many resources, its main use is for simple tasks, you are free to expand on this!

Why Did you make this resource?

This is made for other people that do not want to use zoneplus for something as simple as entering and exiting a zone!

I also made this as my first community post, I had free time to & I saw the opportunity to. It did not take long, this is open for any use & I’d take any criticsm to improve the way I make things.

:star:| Get Here | :backhand_index_pointing_right: RegionManager - Creator Store | Please Report any issues you find!

16 Likes

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