ZonePlus v3.2.0 | Construct dynamic zones and effectively determine players and parts within their boundaries

Disclaimer: Updates are planned to modernize ZonePlus for 2026 - you can listen out for these at our community server - please note inbetween I won’t be monitoring this thread or codebase

ZonePlus v3

:floppy_disk: Source Code | :open_book: Documentation | :ocean: Playground

ZonePlus utilises the new Spatial Query API and Whitelists to effectively determine players, parts and custom items within a zone. You can have as many zones as you like, as large as you like, with zero-to-minimal additional cost to performance.

For those looking to benefit from v3 you can find a detailed breakdown here.

-- This constructs a zone based upon a group of parts in Workspace and listens for when a player enters and exits this group
-- There are also the ``zone.localPlayerEntered`` and ``zone.localPlayerExited`` events for when you wish to listen to only the local player on the client
local Zone = require(game:GetService("ReplicatedStorage").Zone)
local container = workspace.AModelOfPartsRepresentingTheZone
local zone = Zone.new(container)

zone.playerEntered:Connect(function(player)
    print(("%s entered the zone!"):format(player.Name))
end)

zone.playerExited:Connect(function(player)
    print(("%s exited the zone!"):format(player.Name))
end)
-- This constructs a zone based upon a region, tracks a Zombie NPC, then listens for when the item (aka the Zombie) enters and exits the zone.
local Zone = require(game:GetService("ReplicatedStorage").Zone)
local zoneCFrame = CFrame.new()
local zoneSize = Vector3.new(100, 100, 100)
local zone = Zone.fromRegion(zoneCFrame, zoneSize)

zone.itemEntered:Connect(function(item)
    print(("%s entered the zone!"):format(item.Name))
end)

zone.itemExited:Connect(function(item)
    print(("%s exited the zone!"):format(item.Name))
end)

zone:trackItem(workspace.ZombieNPC)
-- This is a one-time use method which calls the given function when the given item enters the zone
local item = character:FindFirstChild("HumanoidRootPart")
zone:onItemEnter(item, function()
    print("The item has entered the zone!")
end)

:sparkles: What’s new


:clapper: Get Started


:package: Installation


:bulb: API


:rocket: Examples

Safe Zone

Paint Zone

Coin Spawner

ZUS5xhQ

Voting Pads

WIslioQ

Random Part Generator

f4be5f98a1f9e52e747dbd994b471c98

Ambient Areas

NPC Damager

Sliding Doors


:iphone: Tutorial


Credit

Big thanks to these people for their resources and contributions:


Final

ZonePlus is free and open source! You’re welcome to use and modify this for any of your projects. Credit back to this thread is greatly appreciated although I won’t send the popo after you if you don’t. :policeman:

I won’t be able to respond to all questions, however I pinky-promise that I’ll read your comments. We’ve put hundreds of hours into this resource so its always a joy to hear your feedback!

That’s all amigos, enjoy :pineapple:

1321 Likes

Really nice work you’ve done
Keep it up :+1:

46 Likes

This is awesome! I will definitely be using this for my games. Thanks!

33 Likes

Great to see performance optimizations and new features, will definetely updating v1 to v2. Thanks for releasing this!

15 Likes

Fantasic! I am definitly using this to create dynamic lighting zones such as when entering a desert zone or a cave!

Thanks again @ForeverHD

19 Likes

I was definitely pondering about subdividing a world into “zones”, where each zone would have its own environment effects and contextual information, such as a spooky forest with a boss fight.

This resource you shared suggests to me that design is not a bad idea.

17 Likes

I’m pretty new to scripting lua
So I need a bit of help w/ my script if u don’t mind
Basically I want to player to be given a tool once entered. And the tool destroyed once they leave the zone

local Zone = require(game:GetService("ReplicatedStorage").Zone)
local group = workspace.areanas._1.swordzone
local zone = Zone.new(group)
local Players = game:GetService("Players")

local Sword = game.ServerStorage.ClassicSword
local char = Players.Character

zone.partEntered:Connect(function(plr)
	for _,v in pairs(char.Character:GetChildren()) do
		if v.Name == Sword.Name then
			return
		end
	end    
	for _,v in pairs(char.Backpack:GetChildren()) do
		if v.Name == Sword.Name then
			return
		end
	end
	Sword:Clone().Parent = plr.Character
end)

zone.playerExited:Connect(function(plr)
	if not char then return end
	for _,v in pairs(char:GetChildren()) do
		if v.Name == Sword.Name then
			v:Destroy()
		end
	end
	for _,v in pairs(char.Backpack:GetChildren()) do
		if v.Name == Sword.Name then
			v:Destroy()
		end
	end
end)

Character is not a valid member of Players "Players"
Script 'ServerScriptService.Script', Line 7 - local char = Players.Character

I copied and pasted parts of my old zone script a friend of mine made for me so idk if it will still work

14 Likes

Great work! Can i use in my game?

12 Likes

For zonePlus specifically there’s one bug I can see at a glance:

For this scenario you’ll want to use zone.playerEntered instead of partEntered.

For the entire script itself, your code breaks as you’re trying to reference Character for the PlayerService (whereas you can only do this on a player instance), try something like this instead:

local Zone = require(game:GetService("ReplicatedStorage").Zone)
local group = workspace.areanas._1.swordzone
local zone = Zone.new(group)
local swordName = "ClassicSword"
local sword = game:GetService("ServerStorage")[swordName]

zone.playerEntered:Connect(function(plr)
	local char = plr.Character
	local foundSword = char:FindFirstChild(swordName) or plr.Backpack:FindFirstChild(swordName)
	if not foundSword then
		sword:Clone().Parent = plr.Character
	end
end)

zone.playerExited:Connect(function(plr)
	local char = plr.Character
	local foundSword = char:FindFirstChild(swordName) or plr.Backpack:FindFirstChild(swordName)
	if foundSword then
		foundSword:Destroy()
	end
end)
12 Likes

Sure you’re free to use and modify the project in anyway you like - simply provide credit and link back to this thread where possible!

13 Likes

Excellent Work! I am using this awesome module to make a checkpoint system. I have a question! There is any method you already made that can return which “part” player entered?

Like I have few checkpoint C1, C2, C3 etc… in the same folder.
If player entered C3 without entered C2 before. Player checkpoint should still C1. So I think I need to check which “part” player exactly entered.

There is my code I already write I think this would work too.

local checkPointGroup = game.Workspace.Map1.Zones.CheckpointZone:GetChildren()
for _, checkPoint in ipairs(checkPointGroup) do
	local checkpointZone = Zone.new(checkPoint)
	checkpointZone.playerEntered:Connect(function(player)
		print(player.Name .. " Entered " .. checkPoint.Name)
	end)
	
	checkpointZone.playerExited:Connect(function(player)
		print(player.Name .. " Exited " .. checkPoint.Name)
	end)
end

But just wondering is there is a method or other way can return which “part” player entered? Thank you for this awesome module!

8 Likes

When the player enters, you could iterate through it’s character parts and call zone:findPart(basePart) to determine which of its parts are within the zone.

Alternatively you could use the partEntered and partExited events when BasePart.CanTouch goes live.


@Maurity_Dev Sure that’s perfectly fine

7 Likes

No clue why but Zone+ V2 doesnt even work for me, I’ve tried to do a safezone, tampered with it, then went to your original script and used it but it still didn’t work. It could be because I put the spawn into the safezone?

9 Likes

Great job with the update and optimizations :+1:

Just updated and logged it into my patch notes for my game’s next major update.

9 Likes

v2 isn’t backwards compatible with v1 (due to the new optimisations which require a different API) therefore if you’re using code from v1 this is likely why.

It’s also important to note that the additionalHeight parameter has been removed, therefore the group-parts that make up the zone have to encapsulate the entire area of the zone.

You can view coded examples for v2 at the new playground, including a safe zone example, and for more information you can visit the docs.

7 Likes

I have V2 but I didn’t remember to remove additionalHeight, thank you so much for getting a quick response. Honestly, I enjoy zone+ V2, good job making it.

5 Likes

Hello, just want to let you know that the link for the API takes you to “https://1foreverhd.github.io/ZonePlus/zone/”, which is an Error 404, instead of “Zone - ZonePlus”.

5 Likes

Quick question;

Whenever I click play, both in Studio and in real servers, the descendant BaseParts of my ZoneGroup get destroyed. Do you know of a solution to this issue?
There are no other scripts in the game at all, and it’s currently just meshes and baseparts in the game.

My script is as follows:

local Zone = require(game:GetService("ReplicatedStorage").Zone)
local zoneGroup = workspace.LobbyZoneGroup
local zone = Zone.new(zoneGroup)

zone.localPlayerEntered:Connect(function()
	PlayThing()
end)

The ZoneGroup is as follows:
75fc3e91dc343a30957775c8687cb040

4 Likes

ZonePlus shouldn’t be destroying any descendant baseparts. Can you provide a video showing the workspace before and after, or DM me a stripped-down place file with the zone where parts are being destroyed.

4 Likes

This dude is doing the impossible! Keep up the good work :smiley:

5 Likes