Skip to content

Commit 948f9e2

Browse files
committed
Fixed character parts memory leak and added zone:onItemEnter/Exit methods
1 parent 04d6483 commit 948f9e2

8 files changed

Lines changed: 219 additions & 27 deletions

File tree

docs/api/zone.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,34 @@ zone:relocate()
128128
```
129129
Moves the zone outside of workspace into a separate WorldModel within ReplicatedStorage or ServerStorage. This action is irreversible - once called it cannot be undone.
130130

131+
----
132+
#### onItemEnter
133+
```lua
134+
zone:onItemEnter(characterOrBasePart, callbackFunction)
135+
```
136+
Tracks the item until it has entered the zone, then calls the given function. If the item is already within the zone, the given function is called right away.
137+
138+
```lua
139+
local item = character:FindFirstChild("HumanoidRootPart")
140+
zone:onItemEnter(item, function()
141+
print("The item has entered the zone!"))
142+
end)
143+
```
144+
145+
----
146+
#### onItemExit
147+
```lua
148+
zone:onItemExit(characterOrBasePart, callbackFunction)
149+
```
150+
Tracks the item until it has exited the zone, then calls the given function. If the item is already outside the zone, the given function is called right away.
151+
152+
```lua
153+
local item = character:FindFirstChild("HumanoidRootPart")
154+
zone:onItemExit(item, function()
155+
print("The item has exited the zone!"))
156+
end)
157+
```
158+
131159
----
132160
#### destroy
133161
```lua

docs/changelog.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## [3.2.0] - September 7 2021
2+
### Added
3+
- ``Zone:onItemEnter(characterOrBasePart, callbackFunction)``
4+
- ``Zone:onItemExit(characterOrBasePart, callbackFunction)``
5+
- An error warning when a zone is constructed using parts that don't belong to the Default collision group
6+
- Support for non-basepart HeadParts
7+
8+
### Changed
9+
- Reorganised checker parts
10+
11+
### Fixed
12+
- A bug preventing the disconnection of tracked character parts which resulted in a slight memory leak whenever a player reset or changed bodyparts
13+
14+
15+
16+
--------
117
## [3.1.0] - August 28 2021
218
### Added
319
- ``Zone.fromRegion(cframe, size)``
@@ -148,4 +164,35 @@
148164

149165
### Fixed
150166
- Rotational and complex geometry detection
151-
- ``getRandomPoints()`` inaccuracies
167+
- ``getRandomPoints()`` inaccuracies
168+
169+
170+
171+
```
172+
-- This constructs a zone based upon a group of parts in Workspace and listens for when a player enters and exits this group
173+
local container = workspace.AModelOfPartsRepresentingTheZone
174+
local zone = Zone.new(container)
175+
176+
zone.playerEntered:Connect(function(player)
177+
print(("%s entered the zone!"):format(player.Name))
178+
end)
179+
180+
zone.playerExited:Connect(function(player)
181+
print(("%s exited the zone!"):format(player.Name))
182+
end)
183+
```
184+
185+
```
186+
-- 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.
187+
local zoneCFrame = CFrame.new()
188+
local zoneSize = Vector3.new(100, 100, 100)
189+
local zone = Zone.fromRegion(zoneCFrame, zoneSize)
190+
191+
zone.itemEntered:Connect(function(item)
192+
print(("%s entered the zone!"):format(item.Name))
193+
end)
194+
195+
zone.itemExited:Connect(function(item)
196+
print(("%s exited the zone!"):format(item.Name))
197+
end)
198+
```

docs/examples.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,9 @@
3333

3434
-------------------------------------
3535

36+
### Sliding Doors
37+
<video src="https://giant.gfycat.com/PastelPastBlueshark.mp4" width="100%" controls></video>
38+
39+
-------------------------------------
40+
3641
All examples can be tested, viewed and edited at the [Playground](https://www.roblox.com/games/6166477769/ZonePlus-Playground).

docs/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[Zone API]: https://1foreverhd.github.io/ZonePlus/api/zone/
55
[Accuracy Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Accuracy.lua
66
[Detection Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Detection.lua
7+
[zone:relocate()]: https://1foreverhd.github.io/ZonePlus/api/zone/#relocate
78

89
## Summary
910

@@ -44,7 +45,10 @@ end)
4445
On the client you may only wish to listen for the LocalPlayer (such as for an ambient system). To achieve this you would alternatively use the ``.localPlayer`` events.
4546

4647
!!! important
47-
Zone group parts must remain within the workspace for zones to fully work
48+
Initially zone parts should be located within Workspace to function properly. If you wish to move zones outside of Workspace (e.g. to prevent them interacting with other parts), consider using [zone:relocate()].
49+
50+
!!! important
51+
Zone parts must belong to the 'Default' (0) collision group.
4852

4953
If you don't intend to frequently check for items entering and exiting a zone, you can utilise zone methods:
5054

src/Zone/VERSION.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
-- v3.1.0
1+
-- v3.2.0

src/Zone/ZoneController/Tracker.lua

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ local players = game:GetService("Players")
55
local runService = game:GetService("RunService")
66
local heartbeat = runService.Heartbeat
77
local Signal = require(script.Parent.Parent.Signal)
8+
local Janitor = require(script.Parent.Parent.Janitor)
89

910

1011

@@ -43,6 +44,9 @@ function Tracker.getCharacterSize(character)
4344
local head = character and character:FindFirstChild("Head")
4445
local hrp = character and character:FindFirstChild("HumanoidRootPart")
4546
if not(hrp and head) then return nil end
47+
if not head:IsA("BasePart") then
48+
head = hrp
49+
end
4650
local headY = head.Size.Y
4751
local hrpSize = hrp.Size
4852
local charSize = (hrpSize * Vector3.new(2, 2, 1)) + Vector3.new(0, headY, 0)
@@ -66,6 +70,7 @@ function Tracker.new(name)
6670
self.characters = {}
6771
self.baseParts = {}
6872
self.exitDetections = {}
73+
self.janitor = Janitor.new()
6974

7075
if name == "player" then
7176
local function updatePlayerCharacters()
@@ -178,7 +183,7 @@ function Tracker:update()
178183
self.parts = {}
179184
self.partToItem = {}
180185
self.items = {}
181-
186+
182187
-- This tracks the bodyparts of a character
183188
for character, _ in pairs(self.characters) do
184189
local charSize = Tracker.getCharacterSize(character)
@@ -188,21 +193,28 @@ function Tracker:update()
188193
local rSize = charSize
189194
local charVolume = rSize.X*rSize.Y*rSize.Z
190195
self.totalVolume += charVolume
196+
197+
local characterJanitor = self.janitor:add(Janitor.new(), "destroy", "trackCharacterParts-"..self.name)
198+
local function updateTrackerOnParentChanged(instance)
199+
characterJanitor:add(instance.AncestryChanged:Connect(function()
200+
if not instance:IsDescendantOf(game) then
201+
if instance.Parent == nil and characterJanitor ~= nil then
202+
characterJanitor:destroy()
203+
characterJanitor = nil
204+
self:update()
205+
end
206+
end
207+
end), "Disconnect")
208+
end
209+
191210
for _, part in pairs(character:GetChildren()) do
192211
if part:IsA("BasePart") and not Tracker.bodyPartsToIgnore[part.Name] then
193212
self.partToItem[part] = character
194213
table.insert(self.parts, part)
195-
local connection
196-
local isDisconnected = false
197-
connection = part:GetPropertyChangedSignal("Parent"):Connect(function()
198-
if part.Parent == nil and not isDisconnected then
199-
isDisconnected = true
200-
connection:Disconnect()
201-
self:update()
202-
end
203-
end)
214+
updateTrackerOnParentChanged(part)
204215
end
205216
end
217+
updateTrackerOnParentChanged(character)
206218
table.insert(self.items, character)
207219
end
208220

@@ -215,7 +227,7 @@ function Tracker:update()
215227
table.insert(self.parts, additionalPart)
216228
table.insert(self.items, additionalPart)
217229
end
218-
230+
219231
-- This creates the whitelist so that
220232
self.whitelistParams = OverlapParams.new()
221233
self.whitelistParams.FilterType = Enum.RaycastFilterType.Whitelist

src/Zone/ZoneController/init.lua

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,19 @@ function ZoneController.getGroup(settingsGroupName)
516516
return settingsGroups[settingsGroupName]
517517
end
518518

519+
local workspaceContainer
520+
local workspaceContainerName = string.format("ZonePlus%sContainer", (runService:IsClient() and "Client") or "Server")
521+
function ZoneController.getWorkspaceContainer()
522+
local container = workspaceContainer or workspace:FindFirstChild(workspaceContainerName)
523+
if not container then
524+
container = Instance.new("Folder")
525+
container.Name = workspaceContainerName
526+
container.Parent = workspace
527+
workspaceContainer = container
528+
end
529+
return container
530+
end
531+
519532

520533

521534
return ZoneController

src/Zone/init.lua

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,8 @@ function Zone.new(container)
7474
self.trackedItems = {}
7575
self.settingsGroupName = nil
7676
self.worldModel = workspace
77-
78-
local checkerPart = janitor:add(Instance.new("Part"), "Destroy")
79-
checkerPart.Size = Vector3.new(0.1, 0.1, 0.1)
80-
checkerPart.Name = "ZonePlusCheckerPart"
81-
checkerPart.Anchored = true
82-
checkerPart.Transparency = 1
83-
checkerPart.CanCollide = false
84-
checkerPart.Parent = workspace
85-
self.checkerPart = checkerPart
77+
self.onItemDetails = {}
78+
self.itemsToUntrack = {}
8679

8780
-- This updates _currentEnterDetection and _currentExitDetection right away to prevent nil comparisons
8881
ZoneController.updateDetection(self)
@@ -344,10 +337,19 @@ function Zone:_update()
344337
end
345338
end
346339
local partProperties = {"Size", "Position"}
340+
local function verifyDefaultCollision(instance)
341+
if instance.CollisionGroupId ~= 0 then
342+
error("Zone parts must belong to the 'Default' (0) CollisionGroup! Consider using zone:relocate() if you wish to move zones outside of workspace to prevent them interacting with other parts.")
343+
end
344+
end
347345
for _, part in pairs(zoneParts) do
348346
for _, prop in pairs(partProperties) do
349347
self._updateConnections:add(part:GetPropertyChangedSignal(prop):Connect(update), "Disconnect")
350348
end
349+
verifyDefaultCollision(part)
350+
self._updateConnections:add(part:GetPropertyChangedSignal("CollisionGroupId"):Connect(function()
351+
verifyDefaultCollision(part)
352+
end), "Disconnect")
351353
end
352354
local containerEvents = {"ChildAdded", "ChildRemoved"}
353355
for _, holder in pairs(holders) do
@@ -592,13 +594,36 @@ function Zone:findPart(part)
592594
return false
593595
end
594596

597+
function Zone:getCheckerPart()
598+
local checkerPart = self.checkerPart
599+
if not checkerPart then
600+
checkerPart = self.janitor:add(Instance.new("Part"), "Destroy")
601+
checkerPart.Size = Vector3.new(0.1, 0.1, 0.1)
602+
checkerPart.Name = "ZonePlusCheckerPart"
603+
checkerPart.Anchored = true
604+
checkerPart.Transparency = 1
605+
checkerPart.CanCollide = false
606+
self.checkerPart = checkerPart
607+
end
608+
local checkerParent = self.worldModel
609+
if checkerParent == workspace then
610+
checkerParent = ZoneController.getWorkspaceContainer()
611+
end
612+
if checkerPart.Parent ~= checkerParent then
613+
checkerPart.Parent = checkerParent
614+
end
615+
return checkerPart
616+
end
617+
595618
function Zone:findPoint(positionOrCFrame)
596619
local cframe = positionOrCFrame
597620
if typeof(positionOrCFrame) == "Vector3" then
598621
cframe = CFrame.new(positionOrCFrame)
599622
end
600-
self.checkerPart.CFrame = cframe
601-
local methodName, args = self:_getRegionConstructor(self.checkerPart, self.overlapParams.zonePartsWhitelist)
623+
local checkerPart = self:getCheckerPart()
624+
checkerPart.CFrame = cframe
625+
--checkerPart.Parent = self.worldModel
626+
local methodName, args = self:_getRegionConstructor(checkerPart, self.overlapParams.zonePartsWhitelist)
602627
local touchingZoneParts = self.worldModel[methodName](self.worldModel, unpack(args))
603628
--local touchingZoneParts = self.worldModel:GetPartsInPart(self.checkerPart, self.overlapParams.zonePartsWhitelist)
604629
if #touchingZoneParts > 0 then
@@ -713,6 +738,9 @@ function Zone:trackItem(instance)
713738
if self.trackedItems[instance] then
714739
return
715740
end
741+
if self.itemsToUntrack[instance] then
742+
self.itemsToUntrack[instance] = nil
743+
end
716744

717745
local itemJanitor = self.janitor:add(Janitor.new(), "destroy")
718746
local itemDetail = {
@@ -723,8 +751,8 @@ function Zone:trackItem(instance)
723751
}
724752
self.trackedItems[instance] = itemDetail
725753

726-
itemJanitor:add(instance:GetPropertyChangedSignal("Parent"):Connect(function()
727-
if instance.Parent == nil then
754+
itemJanitor:add(instance.AncestryChanged:Connect(function()
755+
if not instance:IsDescendantOf(game) then
728756
self:untrackItem(instance)
729757
end
730758
end), "Disconnect")
@@ -782,6 +810,61 @@ function Zone:relocate()
782810
relocationContainer.Parent = worldModel
783811
end
784812

813+
function Zone:_onItemCallback(eventName, desiredValue, instance, callbackFunction)
814+
local detail = self.onItemDetails[instance]
815+
if not detail then
816+
detail = {}
817+
self.onItemDetails[instance] = detail
818+
end
819+
if #detail == 0 then
820+
self.itemsToUntrack[instance] = true
821+
end
822+
table.insert(detail, instance)
823+
self:trackItem(instance)
824+
825+
local function triggerCallback()
826+
callbackFunction()
827+
if self.itemsToUntrack[instance] then
828+
self.itemsToUntrack[instance] = nil
829+
self:untrackItem(instance)
830+
end
831+
end
832+
833+
local inZoneAlready = self:findItem(instance)
834+
if inZoneAlready == desiredValue then
835+
triggerCallback()
836+
else
837+
local connection
838+
connection = self[eventName]:Connect(function(item)
839+
if connection and item == instance then
840+
connection:Disconnect()
841+
connection = nil
842+
triggerCallback()
843+
end
844+
end)
845+
--[[
846+
if typeof(expireAfterSeconds) == "number" then
847+
task.delay(expireAfterSeconds, function()
848+
if connection ~= nil then
849+
print("EXPIRE!")
850+
connection:Disconnect()
851+
connection = nil
852+
triggerCallback()
853+
end
854+
end)
855+
end
856+
--]]
857+
end
858+
end
859+
860+
function Zone:onItemEnter(...)
861+
self:_onItemCallback("itemEntered", true, ...)
862+
end
863+
864+
function Zone:onItemExit(...)
865+
self:_onItemCallback("itemExited", false, ...)
866+
end
867+
785868
function Zone:destroy()
786869
self:unbindFromGroup()
787870
self.janitor:destroy()

0 commit comments

Comments
 (0)