Has anyone used CollectionService to dynamically build/destroy zones? I’m able to add new zones easily but destroying them I found a little more challenging. In a nut shell: how do I determine which zone to destroy given the basepart (container) that is being removed by the collection service?
It is totally possible to create dynamic zones using CollectionService though some testing would be needed on your end to determine the best method.
If your zone only contains one part (The part being added being the zone itself), then it’s pretty straight forward, you can cache the part into some array when GetInstanceAddedSignal is fired.
local zoneForPart = {
-- ex: [ref to Part23] = zone
} :: { [Instance]: Zone }
And upon CollectionService GetInstanceRemovedSignal, simply destroy the zone using zoneForPart[part]:destroy()
This way you’re able to destroy the zone associated to this part.
Though please note: If your zone will be destroyed instead of being removed from the tag, I found a post from HD that said that:
For our next update we’ll have zones automatically destroy/cleanup themselves if their container/descendants are completely destroyed, however for the time being you must ensure you’re calling
zone:destroy()
So if this is your case, it could be avoided altogether.
If your case considers 1 zone but multiple parts being into that zone, things get more complicated and the API doesn’t mention any methods for dynamically adding / removing parts from 1 zone, however I found this post which might help you: ZonePlus v3.2.0 | Construct dynamic zones and effectively determine players and parts within their boundaries - #314 by laughablehaha
Best of luck
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Events = ReplicatedStorage:WaitForChild("Events")
local Modules = ReplicatedStorage:WaitForChild("Modules")
local Manager = require(game:GetService("ServerScriptService"):WaitForChild("DataService"):WaitForChild("DataManager"))
local ZonePlus = require(Modules:WaitForChild("Zone"))
local Variants = workspace:WaitForChild("Variants")
local boughtData = game:GetService("DataStoreService"):GetDataStore("VariantCopies")
local module = {}
-- Functions
function module:OwnsVariant(player, variantName)
local success, result = pcall(function()
return boughtData:GetAsync(player.UserId .. "_" .. variantName)
end)
if success then
print("Data retrieved successfully for player:", player.Name, "Variant:", variantName, "Result:", result)
return result and result > 0
else
warn("Failed to get data for player:", player.Name, "Variant:", variantName, result)
return false
end
end
function module:setupVariants()
for _, variant in pairs(Variants:GetChildren()) do
local name = variant.Name
local initialQuantity = variant:WaitForChild("Quantity").Value
local productId = variant:WaitForChild("ProductId").Value
local variantZone = ZonePlus.new(variant:WaitForChild("Zone"))
variantZone.playerEntered:Connect(function(player)
print("Player entered zone for variant:", name, "Player:", player.Name)
local copiesLeft = initialQuantity
local success, result = pcall(function()
return boughtData:GetAsync("CopiesSold_" .. name)
end)
if success and result ~= nil then
copiesLeft = initialQuantity - result
print("Copies left for variant:", name, "Copies left:", copiesLeft)
else
warn("Failed to get copies sold for variant:", name, result)
end
Events:WaitForChild("PromptVariant"):FireClient(player, name, copiesLeft, productId)
end)
variantZone.playerExited:Connect(function(player)
print("Player exited zone for variant:", name, "Player:", player.Name)
Events:WaitForChild("PromptVariant"):FireClient(player, nil)
end)
-- Initialize copies sold in DataStore if not already set
local successInit, resultInit = pcall(function()
return boughtData:GetAsync("CopiesSold_" .. name)
end)
if successInit and resultInit == nil then
local successSet, resultSet = pcall(function()
boughtData:SetAsync("CopiesSold_" .. name, 0)
end)
if not successSet then
warn("Failed to set initial copies sold for variant:", name, resultSet)
else
print("Initialized copies sold for variant:", name)
end
elseif not successInit then
warn("Failed to initialize copies sold for variant:", name, resultInit)
end
end
end
function module:VariantBought(userId, variantName)
local success, result = pcall(function()
return boughtData:IncrementAsync(userId .. "_" .. variantName, 1)
end)
if success then
print("Variant bought recorded for user:", userId, "Variant:", variantName)
local successCopies, resultCopies = pcall(function()
return boughtData:IncrementAsync("CopiesSold_" .. variantName, 1)
end)
if not successCopies then
warn("Failed to increment copies sold for variant:", variantName, resultCopies)
end
else
warn("Failed to record variant bought for user:", userId, "Variant:", variantName, result)
end
end
function module:init()
print("Initializing Variants Module")
module:setupVariants()
end
return module
In this script zone plus is causing the script that required the module to require this module to freeze and not continue past requiring it. I know it is zone plus because of my extensive testing and confusion. Any help is appreciated. It isn’t by how I used zoneplus from my testing, it was simply requiring it that caused the issues.
That’s a great idea to keep the array and I will give it a try.
What should I do if I have multiple zones of the same name?
PRIORITY TO ZONES
I’m surprised this wasn’t already a feature but I’ve kind of figured out a way to give priority to a zone inside of a zone. This is sort of a hacky solution and could be buggy but I haven’t done enough testing to confirm.
-
Define what priority is
-
Make sure your zones are grouped and the “onlyEnterOnceExitedAll” configuration is enabled.
-
In the ZoneController heartbeat function, add this for loop and redefine the occupantToKeepZone variable to MainComparing.
I don’t know if this works with a zone inside a zone inside a zone but it does work with a zone inside a zone.
hi, i tried them all and find the best way to be: use a folder for zones. Which allows you to make zones a lot more powerfully. If you want like a music zone inside a atmosphere zone, group them differently with :setGroup.
Do you have to use Zone.new everytime in a script or is there a way to locate zones with scripts??
well that’s so annoying. I was doing local parts = zone:getParts()
cause i wanted to detect a humanoid rig and it wasn’t doing the thing
It been a long time since there were any update release to this module but if there a chance that this reply was read can ZonePlus get updated with Typechecking would reduce the time needed for me to go search for documentation.
hello, can you update this zoneplus for Custom rigs? I mean, the rig doesnt have Humanoid instance
like this ^^^
cuz I found a bug (if its bug, cuz i think its bug) that zoneplus can’t detect custom rigs without humanoid
thanks!
I have actually just today, begun a soft-rewrite/restructure of ZonePlus with that in mind. And I would be happy to release it if its wanted. That is, if I actually finish it.
Edit: One of the messiest and complicated code bases I’ve ever seen
idk if ur still working on it but could u include a fix for the problem posted above you:
The post with typechecking? I don’t understand exactly what you’re saying.
no i was replying to someone else while quoting you, hoping they’d include a fix in their version of zoneplus.
Sorry if this feature request has been posted before, still new to these forums and my search skills are lacking…
I’ve been using ZonePlus to swap out some player tracking code in my game and it’s been working great! However, I do think one feature would be a huge quality of life adjust: passing the zone to the callbacks as-well, could be optional or a secondary callback.
Example:
local Zone = require(game:GetService("ReplicatedStorage").Zone)
local container = workspace.AModelOfPartsRepresentingTheZone
local zone = Zone.new(container)
zone.playerEntered:Connect(function(player, zone)
print(("%s entered zone %s!"):format(player.Name, zone.region)) -- zone.region doesn't really make sense here, just an example
end)
For my use case, knowing the zone the user is intersecting is pivotal. I was able to get this by going through the ZoneController
but IMO that’s a less than optimal solution and also would be problematic for me if it were possible for a user to collide with multiple zones in my game.
Hi, I am getting an error when using zone:trackItem I have not tampered with the Zone Module
Error
ReplicatedStorage.Zone:738: attempt to index nil with Instance - Server - Zone:738
14:52:46.712 Stack Begin - Studio
14:52:46.712 Script 'ReplicatedStorage.Zone', Line 738 - function trackItem - Studio - Zone:738
14:52:46.713 Script 'ServerScriptService.Zone', Line 8 - Studio - Zone:8
14:52:46.713 Stack End - Studio
Sever Script
local zone = require(game.ReplicatedStorage.Zone)
local zonec = require(game.ReplicatedStorage.Zone.ZoneController)
local TeleportsSmile = {game.Workspace.Map.Zones.TelportSmile.TeleportSmile}
local TeleportsSmileZone = zone.new(TeleportsSmile)
workspace:WaitForChild("Smile")
zone:trackItem(workspace:WaitForChild("Smile").PrimaryPart)
TeleportsSmileZone.itemEntered:Connect(function(item)
if item.Name == "Smile" or item.Parent.Name == "Smile" then
else
end
end)
Zone Module Error Lines
function Zone:trackItem(instance)
local isBasePart = instance:IsA("BasePart")
local isCharacter = false
if not isBasePart then
isCharacter = instance:FindFirstChildOfClass("Humanoid") and instance:FindFirstChild("HumanoidRootPart")
end
assert(isBasePart or isCharacter, "Only BaseParts or Characters/NPCs can be tracked!")
print(instance)
if self.trackedItems[instance] then
return
end
if self.itemsToUntrack[instance] then
self.itemsToUntrack[instance] = nil
end
local itemJanitor = self.janitor:add(Janitor.new(), "destroy")
local itemDetail = {
janitor = itemJanitor,
item = instance,
isBasePart = isBasePart,
isCharacter = isCharacter,
}
self.trackedItems[instance] = itemDetail
itemJanitor:add(instance.AncestryChanged:Connect(function()
if not instance:IsDescendantOf(game) then
self:untrackItem(instance)
end
end), "Disconnect")
local Tracker = require(trackerModule)
Tracker.itemAdded:Fire(itemDetail)
end
Cool resource, read through the code a bit.
This thing about performance though is only semi-true if you’re using relocate and putting the zones in a different worldmodel, and it still is likely faster to make your own GetPartBoundsInBox() etc
You should probably stress this more.
Whitelisting zones with OverlapParams does effectively nothing for performance if you’re querying the workspace as a worldmodel, because despite what you’d expect - OverlapParams whitelisting/CanQuery/etc. doesn’t actually only search those objects internally - it searches EVERYTHING in the area and then only returns the ones you want back to you.
You can notice this if you run a test with thousands of little parts in an area and perform a GetPartBoundsInBox() query whitelisted to one or two specific parts. It’ll be way slower than if you move the parts you want into a new worldmodel and query using that.
Same way with raycasts
-
If you do a raycast from 5 studs above this part to 5 studs below it
-
If you duplicate that part 1000 times, set all duplicates’ CanX properties to false and set collisiongroup to something that doesnt collide with Default and whitelist only the original part
raycast still becomes over 10 times slower
(In our game we actually just use a custom sparse octree/our own implementation of GetPartBoundsInBox, which also works and is faster for stuff like this than doing the spatial query way with workspace)
Honestly if you want to make the fastest ZonePlus type thing you can(and you’re fine with bounding boxes and don’t need accurate mesh detection like GetPartsInpart), you should probably not be using spatial query and just mimic it with your own structure. It’ll be much faster than roblox’s Spatial Query
To give an update about this,
I have opted to just remake the Resource entirely in my own design, rather than painfully try to rewrite and typecheck what already exists.
Ive made a lot of progress on the System. I have implemented Parallel Lua into the Zones as well.
It wont have every feature upon release as ZonePlus, but it will be a strictly typed, semi optimized, and a good base to implement the missing features off of.