Why it exists
Give server systems a shared way to query which players occupy a named zone or sub-zone.
Spatial membership needs one registration path, repeatable containment checks, and cleanup when players or zones leave.
Based on the Zone System code in the local commons-area repo.
How it works
- 01
Register zones
ZoneService binds tagged instances, creates ZoneClass objects, and indexes IDs and sub-zone relationships.
- 02
Read geometry
ZoneClass collects BaseParts and tests positions in each part's local coordinate space, with optional padding.
- 03
Scan players
A Heartbeat accumulator runs a scan at a 0.2-second interval over tracked players and registered zones.
- 04
Publish membership
The service updates player/zone lookup tables and fires entered or exited callbacks.
- 05
Clean up
Unregistration and player removal clear memberships, indexes, signals, and owned connections.
Selected code
These excerpts are from the paths shown on each patch. Select an annotation for a focused note; copy code directly from each patch.
ZoneService.luau
commons-area/src/server/Services/ZoneService.luau
Scans tracked player roots, marks observed occupants, and exits players not seen in this pass. This is the membership update path.
function ZoneService:_scanZone(zone: any, scanToken: number): (number, number)
local seenPlayers = self.SeenByZone[zone]
if seenPlayers == nil then
seenPlayers = {}
self.SeenByZone[zone] = seenPlayers
end
if not zone:IsEnabled() then
self:_exitAllPlayers(zone)
table.clear(seenPlayers)
return 0, 0
end
local playerChecks = 0
local containsHits = 0
for _, record in self.PlayerRecordsArray do
playerChecks += 1
if self:_refreshPlayerRecord(record) and zone:ContainsPosition(record.Position) then
containsHits += 1
seenPlayers[record.Player] = scanToken
self:_enterZone(record.Player, zone)
end
end
self:_exitUnseenPlayers(zone, seenPlayers, scanToken)
return playerChecks, containsHits
endinit.luau
commons-area/src/server/Modules/Classes/ZoneClass/init.luau
Checks a point against each live region part in local coordinates. This lets rotated parts participate in zone geometry.
function ZoneClass:ContainsPosition(position: Vector3): boolean
local padding = self.Padding
for _, part in self.Parts do
if not part:IsDescendantOf(Workspace) then
continue
end
local localPosition = part.CFrame:PointToObjectSpace(position)
local size = part.Size
if abs(localPosition.X) <= size.X * 0.5 + padding
and abs(localPosition.Y) <= size.Y * 0.5 + padding
and abs(localPosition.Z) <= size.Z * 0.5 + padding
then
return true
end
end
return false
endEngineering decisions
Server-owned membership
- Reason
- The service reads server-side player roots and exposes one membership state to consumers.
- Alternative
- Let each client decide zone membership.
- Tradeoff
- The server performs periodic checks and transitions may be delayed by the scan interval.
Local-space containment
- Reason
- PointToObjectSpace allows a rotated BasePart to define a zone without axis-aligned approximations.
- Alternative
- Compare world-space axis-aligned bounds.
- Tradeoff
- Each containment test transforms the position for each zone part.
Outcome & next steps
Outcome
The source implements tagged zone registration, membership indexes, hierarchy queries, and server-side entry/exit callbacks.
What improved
Containment geometry and membership state are separate modules, so consumers can query zones without reimplementing spatial checks.
Current limitations
- No zone-count benchmark or Studio runtime result was verified in this audit.
- Membership is scanned periodically rather than updated continuously.
Future improvements
- Add a Studio scenario covering rotated parts, nested IDs, removal, and scan timing.
What this demonstrates
Server-side spatial state, zone indexing, signal delivery, and lifecycle cleanup.