From c78d1808b000d758d57322635bd06f6ab754405e Mon Sep 17 00:00:00 2001 From: 1ForeverHD Date: Tue, 23 Feb 2021 16:39:15 +0000 Subject: [PATCH 1/2] Fixed enum bug --- src/Zone/Enum/init.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Zone/Enum/init.lua b/src/Zone/Enum/init.lua index 22b8480..1eb611c 100644 --- a/src/Zone/Enum/init.lua +++ b/src/Zone/Enum/init.lua @@ -66,9 +66,10 @@ function Enum.createEnum(enumName, details) assert(typeof(not enumMetaFunctions[name]), ("bad argument #2.%s.1 - that name is reserved."):format(i, name)) usedNames[tostring(name)] = i local value = detail[2] - assert(typeof(value) == "number" and math.ceil(value)/value == 1, ("bad argument #2.%s.2 - detail value must be an integer!"):format(i)) - assert(typeof(not usedValues[value]), ("bad argument #2.%s.2 - the detail value '%s' already exists!"):format(i, value)) - usedValues[tostring(value)] = i + local valueString = tostring(value) + --assert(typeof(value) == "number" and math.ceil(value)/value == 1, ("bad argument #2.%s.2 - detail value must be an integer!"):format(i)) + assert(typeof(not usedValues[valueString]), ("bad argument #2.%s.2 - the detail value '%s' already exists!"):format(i, valueString)) + usedValues[valueString] = i local property = detail[3] if property then assert(typeof(not usedProperties[property]), ("bad argument #2.%s.3 - the detail property '%s' already exists!"):format(i, tostring(property))) From 8076ef5e067652c2a9627837f33ff89be76b0204 Mon Sep 17 00:00:00 2001 From: 1ForeverHD Date: Fri, 5 Mar 2021 18:53:14 +0000 Subject: [PATCH 2/2] Added Detection support and update docs --- docs/changelog.md | 12 ++++ docs/index.md | 61 ++++++++++++++++++- src/Zone/Enum/Detection.lua | 8 +++ src/Zone/ZoneController.lua | 80 +++++++++++++++++++++---- src/Zone/init.lua | 115 +++++++++++++++++++++++++++++++----- 5 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 src/Zone/Enum/Detection.lua diff --git a/docs/changelog.md b/docs/changelog.md index cdedb1c..ee1706c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,15 @@ +## [2.1.0] - March 5 2021 +### Added +- Detection Enum +- ``zone.enterDetection`` +- ``zone.exitDetection`` +- ``zone:setDetection(enumItemName)`` +- An Optimisation section to Introduction + + + +-------- + ## [2.0.0] - January 19 2021 ### Added - Non-player part checking! (see methods below) diff --git a/docs/index.md b/docs/index.md index 578fec7..5d2ecad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,11 @@ [BasePart.CanTouch]: https://developer.roblox.com/en-us/api-reference/property/BasePart/CanTouch [baseparts]: https://developer.roblox.com/en-us/api-reference/class/BasePart [zone]: https://1foreverhd.github.io/ZonePlus/zone/ -[Zone module docs]: https://1foreverhd.github.io/ZonePlus/zone/ +[Zone API]: https://1foreverhd.github.io/ZonePlus/zone/ +[Accuracy Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Accuracy.lua +[Detection Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Detection.lua + +## Summary ZonePlus is a module enabling the construction of dynamic zones. These zones utilise region checking, raycasting and the new [BasePart.CanTouch] property to effectively determine players and parts within their boundaries. @@ -39,11 +43,64 @@ end) !!! info 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. +!!! important + Zone group parts must remain within the workspace for zones to fully work + If you don't intend to frequently check for items entering and exiting a zone, you can utilise zone methods: ```lua local playersArray = zone:getPlayers() ``` -Discover the full set of methods, events and properties at the [Zone module docs]. +Discover the full set of methods, events and properties at the [Zone API]. + +---- + +## Optimisations +Zones by default perform up to 10 checks per second around a *whole* players character. This behaviour can be changed by modifying the **Accuracy** and **Detection** of zones: + +### Accuracy +This determines the *frequency* of checks per second. + +The accuracy of a zone can be changed two ways with a corresponding [Accuracy Enum]: + +1. Using the ``zone:setAccuracy(itemName)`` method: + ```lua + zone:setAccuracy("High") + ``` + +2. Setting the ``zone.accuracy`` property: + ```lua + zone.accuracy = Zone.enum.Accuracy.High + ``` + +By default accuracy is ``High``. + +!!! info + Modifying the accuracy of one zone may impact the accuracy of another due to the modules collaborative nature. + + +### Detection +This determines the *precision* of checks. + +The way a zone detects players and parts can be changed two ways with a corresponding [Detection Enum]: + +1. Using the ``zone:setDetection(itemName)`` method: + ```lua + zone:setDetection("Automatic") + ``` + +2. Setting the ``zone.enterDetection`` and ``zone.exitDetection`` properties: + ```lua + zone.enterDetection = Zone.enum.Detection.Automatic + zone.exitDetection = Zone.enum.Detection.Automatic + ``` + +By default enterDetection and exitDetection are ``Automatic``. + +!!! info + Modifying the detection of one zone may impact the detection of another due to the modules collaborative nature. + +!!! warning + Setting ``enterDetection`` to (``Zone.enum.Detection.WholeBody`` or ``Zone.enum.Detection.Automatic``) and ``exitDetection`` to ``Zone.enum.Detection.Centre`` will cause the entered and exit events to trigger rapidly when the player lies on the bounds of the zone. diff --git a/src/Zone/Enum/Detection.lua b/src/Zone/Enum/Detection.lua new file mode 100644 index 0000000..bd20822 --- /dev/null +++ b/src/Zone/Enum/Detection.lua @@ -0,0 +1,8 @@ +-- Important note: Precision checks currently only for 'players' and the 'localplayer', not 'parts'. + +-- enumName, enumValue, additionalProperty +return { + {"Automatic", 1}, -- ZonePlus will dynamically switch between 'WholeBody' and 'Centre' depending upon the number of players in a server (this typically only occurs for servers with 100+ players when volume checks begin to exceed 0.5% in script performance). + {"Centre", 2}, -- A tiny lightweight Region3 check will be casted at the centre of the player of part + {"WholeBody", 3}, -- A RotatedRegion3 check will be casted over a player or parts entire body +} \ No newline at end of file diff --git a/src/Zone/ZoneController.lua b/src/Zone/ZoneController.lua index ed05918..259209c 100644 --- a/src/Zone/ZoneController.lua +++ b/src/Zone/ZoneController.lua @@ -63,6 +63,8 @@ local runService = game:GetService("RunService") local heartbeat = runService.Heartbeat local heartbeatConnections = {} local localPlayer = runService:IsClient() and players.LocalPlayer +local playerExitDetections = {} +local WHOLE_BODY_DETECTION_LIMIT = 729000 -- This is roughly the volume where Region3 checks begin to exceed 0.5% in Script Performance @@ -85,12 +87,12 @@ local function fillOccupants(zonesAndOccupantsTable, zone, occupant) end local heartbeatActions = { - ["player"] = function() - return ZoneController._getZonesAndPlayers(activeZones, activeZonesTotalVolume, true) + ["player"] = function(recommendedDetection) + return ZoneController._getZonesAndPlayers(activeZones, activeZonesTotalVolume, true, recommendedDetection) end, - ["localPlayer"] = function() + ["localPlayer"] = function(recommendedDetection) local zonesAndOccupants = {} - local touchingZones = ZoneController.getTouchingZones(localPlayer, true) + local touchingZones = ZoneController.getTouchingZones(localPlayer, true, recommendedDetection) for _, zone in pairs(touchingZones) do if zone.activeTriggers["localPlayer"] then fillOccupants(zonesAndOccupants, zone, localPlayer) @@ -151,6 +153,7 @@ players.PlayerAdded:Connect(function(plr) end) players.PlayerRemoving:Connect(function(plr) updateCharactersTotalVolume() + playerExitDetections[plr] = nil end) @@ -191,6 +194,26 @@ function ZoneController._registerConnection(registeredZone, registeredTriggerTyp end end +-- This decides what to do if detection is 'Automatic' +-- This is placed in ZoneController instead of the Zone object due to the ZoneControllers all-knowing group-minded logic +function ZoneController.updateDetection(zone) + local detectionTypes = { + ["enterDetection"] = "_currentEnterDetection", + ["exitDetection"] = "_currentExitDetection", + } + for detectionType, currentDetectionName in pairs(detectionTypes) do + local detection = zone[detectionType] + if detection == enum.Detection.Automatic then + if charactersTotalVolume > WHOLE_BODY_DETECTION_LIMIT then + detection = enum.Detection.Centre + else + detection = enum.Detection.WholeBody + end + end + zone[currentDetectionName] = detection + end +end + function ZoneController._formHeartbeat(registeredTriggerType) local heartbeatConnection = heartbeatConnections[registeredTriggerType] if heartbeatConnection then return end @@ -205,16 +228,22 @@ function ZoneController._formHeartbeat(registeredTriggerType) local clockTime = os.clock() if clockTime >= nextCheck then local lowestAccuracy + local lowestDetection for zone, _ in pairs(activeZones) do if zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy if lowestAccuracy == nil or zAccuracy < lowestAccuracy then lowestAccuracy = zAccuracy end + ZoneController.updateDetection(zone) + local zDetection = zone._currentEnterDetection + if lowestDetection == nil or zDetection < lowestDetection then + lowestDetection = zDetection + end end end local highestAccuracy = lowestAccuracy - local zonesAndOccupants = heartbeatActions[registeredTriggerType]() + local zonesAndOccupants = heartbeatActions[registeredTriggerType](lowestDetection) for zone, _ in pairs(activeZones) do if zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy @@ -281,7 +310,7 @@ function ZoneController._updateZoneDetails() end end -function ZoneController._getZonesAndPlayers(zonesDictToCheck, zoneCustomVolume, onlyActiveZones) +function ZoneController._getZonesAndPlayers(zonesDictToCheck, zoneCustomVolume, onlyActiveZones, recommendedDetection) local totalZoneVolume = zoneCustomVolume if not totalZoneVolume then for zone, _ in pairs(zonesDictToCheck) do @@ -295,7 +324,7 @@ function ZoneController._getZonesAndPlayers(zonesDictToCheck, zoneCustomVolume, -- then it's more efficient cast regions and rays within each character and -- then determine the zones they belong to for _, plr in pairs(players:GetPlayers()) do - local touchingZones = ZoneController.getTouchingZones(plr, onlyActiveZones) + local touchingZones = ZoneController.getTouchingZones(plr, onlyActiveZones, recommendedDetection) for _, zone in pairs(touchingZones) do if not onlyActiveZones or zone.activeTriggers["player"] then fillOccupants(zonesAndOccupants, zone, plr) @@ -363,8 +392,19 @@ function ZoneController.getCharacterRegion(player) return charRegion, regionCFrame, charSize end -function ZoneController.getTouchingZones(player, onlyActiveZones) - local charRegion = ZoneController.getCharacterRegion(player) +function ZoneController.getTouchingZones(player, onlyActiveZones, recommendedDetection) + local exitDetection = playerExitDetections[player] + playerExitDetections[player] = nil + local finalDetection = exitDetection or recommendedDetection + local charRegion + if finalDetection == enum.Detection.WholeBody then + charRegion = ZoneController.getCharacterRegion(player) + else + local char = player.Character + local hrp = char and char:FindFirstChild("HumanoidRootPart") + local regionCFrame = hrp and hrp.CFrame + charRegion = regionCFrame and RotatedRegion3.new(regionCFrame, Vector3.new(0.1, 0.1, 0.1)) + end if not charRegion then return {} end --[[ local part = Instance.new("Part") @@ -384,10 +424,17 @@ function ZoneController.getTouchingZones(player, onlyActiveZones) local hrp = player.Character.HumanoidRootPart local hrpCFrame = hrp.CFrame local hrpSizeX = hrp.Size.X - local pointsToVerify = { - (hrpCFrame * CFrame.new(-hrpSizeX, 0, 0)).Position, - (hrpCFrame * CFrame.new(hrpSizeX, 0, 0)).Position, - } + local pointsToVerify + if finalDetection == enum.Detection.WholeBody then + pointsToVerify = { + (hrpCFrame * CFrame.new(-hrpSizeX, 0, 0)).Position, + (hrpCFrame * CFrame.new(hrpSizeX, 0, 0)).Position, + } + else + pointsToVerify = { + hrp.Position, + } + end if not ZoneController.verifyTouchingParts(pointsToVerify, parts) then return {} end @@ -398,9 +445,16 @@ function ZoneController.getTouchingZones(player, onlyActiveZones) zonesDict[correspondingZone] = true end local touchingZonesArray = {} + local newExitDetection for zone, _ in pairs(zonesDict) do + if newExitDetection == nil or zone._currentExitDetection < newExitDetection then + newExitDetection = zone._currentExitDetection + end table.insert(touchingZonesArray, zone) end + if newExitDetection then + playerExitDetections[player] = newExitDetection + end return touchingZonesArray end diff --git a/src/Zone/init.lua b/src/Zone/init.lua index 698d0d6..8eff62d 100644 --- a/src/Zone/init.lua +++ b/src/Zone/init.lua @@ -1,6 +1,8 @@ --[[ zone:header [Accuracy Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Accuracy.lua +[Detection Enum]: https://github.com/1ForeverHD/ZonePlus/blob/main/src/Zone/Enum/Detection.lua [setAccuracy]: https://1foreverhd.github.io/ZonePlus/zone/#setaccuracy +[setDetection]: https://1foreverhd.github.io/ZonePlus/zone/#setdetection ## Construtors @@ -57,7 +59,14 @@ Generates random points within the zones region until one falls within its bound ```lua zone:setAccuracy(enumIdOrName) ``` -Sets the frequency of checks based upon the [Accuracy Enum]. +Sets the frequency of checks based upon the [Accuracy Enum]. Defaults to 'High'. + +---- +#### setDetection +```lua +zone:setDetection(enumIdOrName) +``` +Sets the precision of checks based upon the [Detection Enum]. Defaults to 'Automatic'. ---- #### destroy @@ -112,6 +121,9 @@ zone.partEntered:Connect(function(part) end) ``` +!!! info + This event only works for non-anchored parts + !!! warning This connection will not fully optimise *until* [BasePart.CanTouch](https://developer.roblox.com/en-us/api-reference/property/BasePart/CanTouch) goes [live](https://developer.roblox.com/en-us/resources/release-note/Release-Notes-for-460). @@ -123,6 +135,9 @@ zone.partExited:Connect(function(part) end) ``` +!!! info + This event only works for non-anchored parts + !!! warning This connection will not fully optimise *until* [BasePart.CanTouch](https://developer.roblox.com/en-us/api-reference/property/BasePart/CanTouch) goes [live](https://developer.roblox.com/en-us/resources/release-note/Release-Notes-for-460). @@ -133,9 +148,41 @@ end) ## Properties #### accuracy ```lua -local accuracyEnumId = zone.accuracy --[default: 'Enum.enums.Accuracy.High'] +local accuracyEnumId = zone.accuracy --[default: 'Zone.enum.Accuracy.High'] +``` +To change ``accuracy`` you can use [setAccuracy] or do: + +```lua +zone.accuracy = Zone.enum.Accuracy.ITEM_NAME +``` + +A list of Accuracy enum items can be found at [Accuracy Enum]. + +---- +#### enterDetection +```lua +local enterDetection = zone.enterDetection --[default: 'Zone.enum.Detection.Automatic'] +``` +To change both detection types use [setDetection] otherwise to set individually do: + +```lua +zone.enterDetection = Zone.enum.Detection.ITEM_NAME +``` + +A list of Detection enum items can be found at [Detection Enum]. + +---- +#### exitDetection +```lua +local exitDetection = zone.exitDetection --[default: 'Zone.enum.Detection.Automatic'] +``` +To change both detection types use [setDetection] otherwise to set individually do: + +```lua +zone.exitDetection = Zone.enum.Detection.ITEM_NAME ``` -To change ``accuracy`` it's recommended you use [setAccuracy]. + +A list of Detection enum items can be found at [Detection Enum]. ---- #### autoUpdate @@ -190,6 +237,7 @@ Zone.__index = Zone if not referencePresent then ZonePlusReference.addToReplicatedStorage() end +Zone.enum = enum @@ -227,6 +275,11 @@ function Zone.new(group) self.activeTriggers = {} self.occupants = {} self.trackingTouchedTriggers = {} + self.enterDetection = enum.Detection.Automatic + self.exitDetection = enum.Detection.Automatic + self._currentEnterDetection = nil -- This will update automatically internally + self._currentExitDetection = nil -- This will also update automatically internally + self.totalPartVolume = 0 -- Signals self.updated = maid:give(Signal.new()) @@ -414,16 +467,23 @@ function Zone:_update() -- child is removed or added from a container (anything which isn't a basepart) local function update() if self.autoUpdate then - coroutine.wrap(function() - if self.respectUpdateQueue then - updateQueue = updateQueue + 1 - wait(0.1) - updateQueue = updateQueue - 1 - end - if updateQueue == 0 and self.zoneId then - self:_update() + local executeTime = os.clock() + if self.respectUpdateQueue then + updateQueue += 1 + executeTime += 0.1 + end + local updateConnection + updateConnection = runService.Heartbeat:Connect(function() + if os.clock() >= executeTime then + updateConnection:Disconnect() + if self.respectUpdateQueue then + updateQueue -= 1 + end + if updateQueue == 0 and self.zoneId then + self:_update() + end end - end)() + end) end end local partProperties = {"Size", "Position"} @@ -538,6 +598,9 @@ function Zone:_disconnectTouchedConnection(triggerType) end end +local function round(number, decimalPlaces) + return math.round(number * 10^decimalPlaces) * 10^-decimalPlaces +end function Zone:_partTouchedZone(part) local trackingDict = self.trackingTouchedTriggers["part"] if trackingDict[part] then return end @@ -550,6 +613,10 @@ function Zone:_partTouchedZone(part) local partMaid = self._maid:give(Maid.new()) trackingDict[part] = partMaid part.CanTouch = false + -- + local partVolume = round((part.Size.X * part.Size.Y * part.Size.Z), 5) + self.totalPartVolume += partVolume + -- partMaid:give(heartbeat:Connect(function() local clockTime = os.clock() if clockTime >= nextCheck then @@ -578,6 +645,7 @@ function Zone:_partTouchedZone(part) partMaid:give(function() trackingDict[part] = nil part.CanTouch = true + self.totalPartVolume = round((self.totalPartVolume - partVolume), 5) end) end @@ -604,7 +672,8 @@ function Zone:findLocalPlayer() end function Zone:findPlayer(player) - local touchingZones = ZoneController.getTouchingZones(player) + ZoneController.updateDetection(self) + local touchingZones = ZoneController.getTouchingZones(player, false, self._currentEnterDetection) for _, zone in pairs(touchingZones) do if zone == self then return true @@ -659,8 +728,9 @@ function Zone:findPart(part, regionConstructor, enterPosition, timeInZone) end function Zone:getPlayers() + ZoneController.updateDetection(self) local playersArray = {} - local zonesAndOccupants = ZoneController._getZonesAndPlayers({self = true}, self.volume) + local zonesAndOccupants = ZoneController._getZonesAndPlayers({self = true}, self.volume, false, self._currentEnterDetection) local occupantsDict = zonesAndOccupants[self] if occupantsDict then for plr, _ in pairs(occupantsDict) do @@ -727,6 +797,23 @@ function Zone:setAccuracy(enumIdOrName) self.accuracy = enumId end +function Zone:setDetection(enumIdOrName) + local enumId = tonumber(enumIdOrName) + if not enumId then + enumId = enum.Detection[enumIdOrName] + if not enumId then + error(("'%s' is an invalid enumName!"):format(enumIdOrName)) + end + else + local enumName = enum.Detection.getName(enumId) + if not enumName then + error(("%s is an invalid enumId!"):format(enumId)) + end + end + self.enterDetection = enumId + self.exitDetection = enumId +end + function Zone:destroy() self._maid:clean() end