ZoneService v0.5.0 | 4M+ Zones at 240 FPS | Up To 1000x+ faster than ZonePlus

Update 0.4.0 Download

  • API rework: removed all old zone registration methods and all string key usage. New API forces the user to register zones and track entities in correct order. For example, with the new API you cannot try to observe an entity before it’s registered or before a group is registered
  • Added Zone, Group, and Tracked objects
  • Zone and Group objects can be instantiated with .new while Tracked objects are returned when calling :track
  • Zone specific methods (anything that used to require a zone name) moved onto new Zone object
  • Method name changes: isPointInZone → isPointInside, getRandomPointInZone → getRandomPointInside, updateZone → update, removeZone → destroy, entered → onEnter, exited → onExit, changed → onZoneChange, addZone → ZoneService.new, addZoneFromPart → ZoneService.fromPart
  • getSubjectsInZone method removed
  • Subject type changed to “Entity” and all subject variables renamed to entity for more clarity
  • Added metadata and destroyed flag to zones
  • New group creation method via Group.new
  • Add/remove zones to/from groups using new :add and :remove methods
  • Destroy groups using Group:destroy
  • If a zone is not added to any group, it defaults to a default internal group. In this state, zones cannot be observed with :zoneChanged
  • A single zone can now belong to multiple groups, which scales better than creating multiple copies of the same zone under different groups. That said, scaling with multiple groups is still not amazing due to an information limit, so it’s recommended the user does not add the same zone to multiple groups unless absolutely necessary
  • Max groups limit removed
  • Reworked group tracking logic: scalability with number of groups improved significantly
  • Removed unnecessary internal bookkeeping
  • changed signal (now called onZoneChange) moved onto Tracked handle
  • getZones method moved onto Tracked handle
  • Added adaptive polling algorithm: dynamically selects the optimal frame interval, improves performance significantly in some cases
  • Polling interval is no longer configurable; instead, there is a new polling rate config
  • Added strict typing for new type solver

New Benchmarks

Polling rate = 30Hz on both, unlimited frame budget on QuickZone. Same benchmark code as before (except for ZoneService API changes). It goes without saying that the performance is still the same on the light tests, so only heavy tests are retested.

Heavy Test 1 (10K zones, 10K entities)

Library FPS
QuickZone 21.60
ZoneService 200.98

Heavy Test 2 (1M zones, 500 entities)

Library FPS
QuickZone 13.13
ZoneService 54.62

Heavy Test 3 (10K dynamic zones, 10K entities)

Library FPS
QuickZone 14.97
ZoneService 41.43

On heavy test 2 ZoneService is slower due to the new feature that allows the same zone to be under multiple groups. However, thanks to the new adaptive polling feature we can achieve much higher performance (~115 FPS) on this test if we lower the polling rate from 30Hz to 20Hz. I also retested memory usage and the results are the same as before, so I didn’t include them. Likewise, initialization performance is about the same as before or possibly slightly better. The result was still 4.1M zones on the retest.

New API

The API has changed significantly compared to the original release. Therefore I’ve updated the docs to reflect these new changes. Here is an example usage with the new API:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ZoneService = require(ReplicatedStorage.ZoneService)
local Group = ZoneService.Group
local zones = Group.new()

for i, part in workspace.Zones:GetChildren() do
  local zone = ZoneService.fromPart(part, {priority = 10, dynamic = false, metadata = "zone"..i})
  zones:add(zone)

  zone:onEnter(function(player)
    print(player.Name.." entered zone!")
    part.Color = Color3.new(0, 1, 0)
  end

  zone:onExit(function(player)
    print(player.Name.." exited zone!")
    part.Color = Color3.new(0, 0, 0)
  end  
end

Players.PlayerAdded:Connect(function(player)
  local tracked = ZoneService:track(player)	
  tracked:onZoneChange(zones, function(zone)
    print(player.Name.." is in "..tostring(zone and zone.metadata))
  end)
end)

Players.PlayerRemoving:Connect(function(player)
  ZoneService:untrack(player)
end)
4 Likes

Update 0.5.0 Download

Bug fixes

  • Fixed onZoneChange firing nil on zone transitions
  • Fixed onEnter and onExit signals not providing a way to identify the group associated with the fire
  • Fixed zone:update not storing the new size

Features

  • Removed groups entirely, including internal default group special case
  • Added observers as a replacement for groups: onEnter, onExit, and onZoneChange signals have been moved onto observers. Logically, observers work pretty much the same way as groups
  • Removed .ballSize and .cylinderSize helpers
  • Zone objects now expose isDynamic, priority, and observers as read only fields
  • Added Zone collections and associated Zones type
  • Zone collection objects have add, remove, and destroy methods
  • Added .fromZones method for constructing a zone collection (in other words, a union of zones)
  • onEnter and onExit can now observe a collection of zones in addition to a single zone
  • Added Performance Mode: when ZoneService is causing heavy load, automatically reduces the polling rate to increase FPS. Performance Mode is often very worth it as a small reduction in polling rate can lead to a significant increase in FPS
  • Added error message to onZoneChange and getZones when the entity is no longer tracked

New API

Apologies for the API changes. Due to ZoneService being in beta, the API wasn’t fully decided. That being said, this new API should be final and there shouldn’t be any significant changes from now on.

Here’s an example usage:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ZoneService = require(ReplicatedStorage.ZoneService)
local Observer = ZoneService.Observer

local observer = Observer.new()
local zone = ZoneService.new(CFrame.new(10, 10, 10), Vector3.new(20, 20, 20), "Block", {dynamic = false, priority = 20, metadata = "SafeZone"})
observer:add(zone)

observer:onEnter(zone, function(player)
  print(player.Name.." entered the zone!")
end)

observer:onExit(zone, function(player)
  print(player.Name.." exited the zone!")
end)

Players.PlayerAdded:Connect(function(player)
  local tracked = ZoneService:track(player)
  observer:onZoneChange(tracked, function(zone)
    print(player.Name.." is in "..(zone and zone.metadata))
  end)
end)

Players.PlayerRemoving:Connect(function(player)
  ZoneService:untrack(player)
end)

Along with the new API I’ve created a new documentation page (currently WIP)