Skip to content
Engine

World

SpatialService

Named spatial indexes for fast proximity queries, parts that keep their own position up to date, and zones with Entered and Exited events.

ServiceShared

Getting it

local SpatialService = Engine:GetService("SpatialService")

Overview

SpatialService answers "what is near here?" and "who is inside this area?" without looping over everything.

  • An index stores items by position (built on Octree) and can track moving parts by itself.
  • A zone is an area made of spheres and boxes that fires Entered and Exited when players (or items in an index) move in and out.
A safe zone
local SpatialService = Engine:GetService("SpatialService")

local spawnArea = workspace.SpawnArea

local safeZone = SpatialService:CreateZone("SafeZone", {
	{ Type = "Box", CFrame = spawnArea.CFrame, Size = spawnArea.Size },
})

safeZone.Entered:Connect(function(player)
	setInvulnerable(player, true)
end)

safeZone.Exited:Connect(function(player)
	setInvulnerable(player, false)
end)

Indexes

CreateIndex

SpatialService:CreateIndex(name: string, config: { cellSize: number?, bounds: Bounds?, maxItems: number? }?): SpatialIndex

Creates a named index. cellSize is roughly how big the smallest cells should be in studs; the depth of the underlying octree is derived from it. bounds defaults to a cube of DefaultBoundsExtent studs around the origin. Names are unique.

GetIndex

SpatialService:GetIndex(name: string): SpatialIndex?

SpatialIndex

A SpatialIndex has every method of an Octree (Insert, Move, Remove, Contains, GetPosition, QueryRadius, CountInRadius, QueryBox, QueryAABB, QueryRay, Nearest, ForEach, GetAllItems, Clear) plus:

MemberDescription
:TrackPart(part): TrackHandleInserts the part and refreshes its position automatically (DefaultTrackHz times per second). Tracking stops when the part is destroyed or you call handle:Disconnect(). A part can only be tracked once per index.
:Destroy()Stops all tracking and removes the index.
Name, Size, IsEmpty, Bounds, CellSize, DestroyedRead-only.
local enemies = SpatialService:CreateIndex("Enemies", { cellSize = 32 })

for _, npc in workspace.Enemies:GetChildren() do
	enemies:TrackPart(npc.HumanoidRootPart)
end

local function explode(position: Vector3, radius: number)
	for _, root in enemies:QueryRadius(position, radius) do
		damage(root.Parent, 50)
	end
end

Zones

CreateZone

SpatialService:CreateZone(name: string, volumes: { Volume }, config: ZoneConfig?): Zone

Creates a named zone from one or more volumes; a position inside any of them counts as inside the zone.

VolumeFields
Sphere{ Type = "Sphere", Center: Vector3, Radius: number }
Box{ Type = "Box", CFrame: CFrame, Size: Vector3 } (may be rotated)
Region3{ Type = "Region3", Min: Vector3, Max: Vector3 }
ConfigDescription
trackPlayers: boolean?Check every player's character (its HumanoidRootPart, or PrimaryPart). Default from configuration.
index: SpatialIndex?Also check every item in this index.
checkIntervalSeconds: number?Seconds between checks.

Zone

MemberDescription
Entered: Signal<occupant>A player or index item moved inside.
Exited: Signal<occupant>It moved out, left the game, or disappeared from the index.
Occupants: { any }Who is inside right now.
:ContainsPoint(position): booleanWhether a point is inside.
:ContainsAt(at): booleanSame for a Vector3, CFrame, BasePart, Attachment or Model.
:Destroy()Stops checking and removes the zone.
Name, Volumes, DestroyedRead-only.

Occupants are players when they come from trackPlayers, and whatever you inserted when they come from an index.

GetZone / GetZonesAt

SpatialService:GetZone(name: string): Zone?
SpatialService:GetZonesAt(at: Vector3 | CFrame | Instance): { Zone }

GetZonesAt returns every zone that contains the position right now, which is handy for "which area is the player in" on demand.

Configuration

Defaults from DefaultConfiguration/Services/SpatialService6 settings
SettingDefaultWhat it does
DefaultCellSize16Studs; :CreateIndex() aims for leaf cells about this big when config.cellSize is omitted.
DefaultBoundsExtent4096+/- studs on every axis around the origin when config.bounds is omitted.
DefaultMaxIndexDepth12Hard clamp on the octree depth a tiny cellSize/huge bounds could otherwise compute.
DefaultTrackHz15Position-refresh rate of SpatialIndex:TrackPart().
DefaultTrackPlayerstrueWhether a Zone checks every Player's character when config.trackPlayers is omitted.
DefaultCheckIntervalSeconds0.2Seconds between a Zone's re-scans of its candidates for Entered/Exited.

Types

export type SpatialIndex
export type Zone
export type Volume
export type IndexConfig = { cellSize: number?, bounds: Bounds?, maxItems: number? }
export type ZoneConfig = { trackPlayers: boolean?, index: SpatialIndex?, checkIntervalSeconds: number? }