Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction


SimplerZone is the successor to LessSimpleZone & SimpleZone, a versatile & flexible spatial zone module for tracking where and when specific items enter specific areas of your game, doing spatial checks, etc.

To use the module, download the .rbxm binary from the releases of the GitHub.

The general hierarchy of objects inside the SimplerZone library: module hierarchy

Misc Types


A collection of smaller utility types the module uses.

Definition


export type Shape = BoxShape | CylinderShape | SphereShape | WedgeShape | GroupShape | MeshShape
export type Disconnector = () -> ()
export type ZoneItems = {
	parts: {BasePart},
	models: {Model},
	players: {Player},
	attachments: {Attachment},
	pvinstances: {PVInstance},
}
export type ZoneItem = {
	part: BasePart?,
	model: Model?,
	player: Player?,
	attachment: Attachment?,
	pvinstance: PVInstance?,
}

Shape


A Shape is an elementary object that defines, well, the shape of your Zones. Shapes aren't defined by Instances with the exception of MeshShape, so you can create and use them even without explicitly making parts for them inside the workspace.

Most shapes will have a .transform property, this property determines the offset of the Shape from the .cframe of the ShapeInstance that uses it. In the case of SphereShapes, .transform isn't present and instead is replaced by .center

During usage of Shapes within ShapeInstances, it is gauranteed that the properties of your Shapes will not change. So it is okay to reference 1 Shape multiple times

BoxShape


Definition


{
    ty: "Box",
    transform: CFrame,
    size: vector
}

SphereShape


Definition


{
    ty: "Sphere",
    center: vector,
    radius: number
}

CylinderShape


Definition


{
    ty: "Cylinder",
    center: vector,
	axis: vector,
	radius: number,
	transform: CFrame,
	size: vector,
}

WedgeShape


Definition


{
    ty: "Wedge",
	transform: CFrame,
	size: vector,
	vertices: {vector}
}

MeshShape


Definition


{
    ty: "Mesh",
	transform: CFrame,
	size: vector,
	mesh: MeshPart
}

GroupShape


Definition


{
    ty: "Group",
	transform: CFrame,
	shapes: {Shape}
}

ShapeInstance


If Shapes were akin to Mesh Ids, then ShapeInstances would be MeshParts.

ShapeInstances define an instance of a Shape, specifying its world .cframe, and .scale.

Definition


{
    toParts: (ShapeInstance, overrideCFrame: CFrame?) -> {BasePart},
	syncWithPart: (ShapeInstance, part: BasePart) -> Disconnector,
	syncWithAttachment: (ShapeInstance, attachment: Attachment) -> Disconnector,
	attach: (ShapeInstance, zone: Zone) -> (),
	detach: (ShapeInstance) -> (),
	
	cframe: CFrame,
	scale: vector,
	zones: {Zone},
	shape: Shape,
}

Methods


ShapeInstance:attach(zone: Zone) -> ()

Attaches this ShapeInstance to zone, adding to its geometry.

ShapeInstance:detach(zone: Zone) -> ()

Removes this ShapeInstance from the zone, removing from its geometry.

ShapeInstance:detachAll() -> ()

Removes this ShapeInstance from all attached zones

ShapeInstance:toParts(overrideCFrame: CFrame? = CFrame.identity) ->

Creates an array of BaseParts from a ShapeInstance. The world CFrame will be centered around overrideCFrame

ShapeInstance:syncWithPart(part: BasePart) -> ()

Syncs the ShapeInstance with a BasePart. When the BasePart moves, so does the ShapeInstance, updating any Zones it's attached to.

ShapeInstance:syncWithAttachment(attachment: Attachment) -> ()

Syncs the ShapeInstance with an Attachment. When the Attachment moves (.WorldCFrame), so does the ShapeInstance, updating any Zones it's attached to.

Fields


ShapeInstance.shape: Shape

Defines the form/shape of this ShapeInstance.

ShapeInstance.cframe: CFrame

The world CFrame of this ShapeInstance. The .shape field will be transformed by this fields value.

When mutating this field, make sure to call Zone:rebuild() on all Zones inside ShapeInstance.zones so that the change registers properly.

ShapeInstance.scale: vector

The scale modifier to apply to the .shape fields values size.

ShapeInstance.zones:

All Zones that this ShapeInstance is attached to.

Zone


Zones are the main object of the SimplerZone library. Zones are composed of ShapeInstances and contain methods to manipulate/use them.

Definition


{
	getRandomPoint: (Zone) -> vector,
	isPointInZone: (Zone, point: vector) -> boolean,
	isBoxInZone: (Zone, cframe: CFrame, size: vector) -> boolean,
	rebuild: (Zone) -> (),
	rebuildIfDirty: (Zone) -> (),
	
	shapes: {ShapeInstance},
	bvh: Geometry.BvhNode,
	boxes: {Geometry.Box},
	bvhDirty: boolean,
}

Methods


Zone:getRandomPoint() -> vector

Returns a random point inside any of the ShapeInstances this Zone is composed of.

Zone:isPointInZone(point: vector) -> boolean

Checks if point is inside any of the ShapeInstances this Zone is composed of.

Zone:isBoxInZone(cframe: CFrame, size: vector) -> boolean

Checks if a box, defined by cframe and size, is inside any of the ShapeInstances this Zone is composed of.

Zone:rebuild() -> ()

Rebuilds the static geometry of this Zone. Should be called if any of the ShapeInstances this Zone is composed of is mutated in some way.

Zone:rebuildIfDirty() -> ()

Calls Zone:rebuild() if Zone.bvhDirty is true.

Fields

--

Zone.shapes:

The ShapeInstances this Zone is composed of.

Zone.bvh: BvhNode

The root BvhNode this Zone uses to optimize bounds checking.

Zone.bvhDirty: boolean

If Zone.bvh should be rebuilt at some point.

Zone.boxes:

The bounding box of each ShapeInstance in Zone.shapes.

ZoneListener


A ZoneListener is an object that can listen to when certain items enter the geometric boundaries of a Zone defined by its ShapeInstances.

Definition


setmetatable<{
	onEnter: (ZoneListener, fn: (item: ZoneItem) -> ()) -> Disconnector,
	onExit: (ZoneListener, fn: (item: ZoneItem) -> ()) -> Disconnector,
	observe: (ZoneListener, fn: (item: ZoneItem) -> (() -> ())?) -> Disconnector,

	setGroups: (ZoneListener, groups: {TrackGroup}) -> (),

	subscribe: (ZoneListener, zone: Zone) -> (),
	unsubscribe: (ZoneListener, zone: Zone) -> (),
	unsubscribeAll: (ZoneListener) -> (),

	precision: "Part" | "BoundingBox",
	queryShape: "Point" | "Box",
	groups: {TrackGroup},
	zones: {Zone},
	itemsInside: ZoneItems,

	iter: (ZoneListener) -> () -> ZoneItem,
	data: ZoneListenerData
}, {__iter: (ZoneListener) -> () -> ZoneItem}>

Methods


ZoneListener:subscribe(zone: Zone) -> ()

Subscribes this ZoneListener to the Zone, making any item this listener is tracking who enters the geometric boundaries of the Zone fire events.

ZoneListener:unsubscribe(zone: Zone) -> ()

Unsubscribes this ZoneListener from zone, cleaning up any internal listeners and events in the process.

ZoneListener:unsubscribeAll() -> ()

Unsubscribes this ZoneListener from all Zones it's subscribed to.

ZoneListener:onEnter(fn: (item: ZoneItem) -> ()) -> Disconnector

Connects fn to be called when any ZoneItem enters the geometric boundaries of any subscribed Zone

ZoneListener:onExit(fn: (item: ZoneItem) -> ()) -> Disconnector

Connects fn to be called when any ZoneItem exits the geometric boundaries of any subscribed Zone

ZoneListener:observe(fn: (item: ZoneItem) -> (() -> ())?) -> Disconnector

Connects fn to be called when any ZoneItem enters the geometric boundaries of any subscribed Zone, and for the returned function (if any) to be called when it exits it.

ZoneListener:setGroups(groups: {TrackGroup}) -> ()

Sets the TrackGroups that this ZoneListener should be listening for.

ZoneListener:iter() -> () -> ZoneItem

Iterates through all ZoneItems that are geometrically inside the Zones the ZoneListener is currently subscribed to.

Fields


ZoneListener.precision: "Part" | "BoundingBox"

Determines how precise the collision of TrackGroup items should be.

PrecisionDescription
"Part"In player items, model items, pvinstance items etc, the collision will be determined by the collision of their individual parts instead of the bounding box. So for example if a players arm extends into a zone such that the bounding box intersects it but no part actually touches it, this will not trigger an entry event.
"BoundingBox"In player items, model items, pvinstance items etc, the collision will be determined by the collision of their respective bounding boxes instead of the parts they're composed of. This is faster but can cause false-entries such as when specific pokey parts of a model extend the bounding box of it such that the bounding box intersects your zone but no part of it actually does.

ZoneListener.queryShape: "Point" | "Box"

Determines what method should be used to determine if a tracked item is in a zone or not.

| Query Shape | Description | | "Point" | Items will be inside the zone if their center points collide with it. | | "Box" | Items will be inside the zone if their bounding box collides with it. |

ZoneListener.itemsInside: ZoneItems

All the items geometrically inside the Zones this ZoneListener is listening to.

ZoneListener.groups:

TrackGroups being tracked by this ZoneListener.

ZoneListener.zones:

Zones this ZoneListener is subscribed to.

TrackGroup


TrackGroups are containers that contain items that ZoneListeners should be tracking for when they enter/exit the Zones it's listening to.

Definition


{
	update: (TrackGroup) -> (),
	removeFrom: (TrackGroup, listener: ZoneListener) -> (),
	addItem: (TrackGroup, item: ZoneItem) -> (),
	addItems: (TrackGroup, items: ZoneItems) -> (),
	removeItem: (TrackGroup, item: ZoneItem) -> (),
	removeItems: (TrackGroup, items: ZoneItems) -> (),
	clear: (TrackGroup) -> (),
	
	mutable: ZoneItems,
	immutable: ZoneItems,
	listeners: {ZoneListener}
}

Methods


TrackGroup:update() -> ()

Moves items inside TrackGroup.mutable into TrackGroup.immutable, updating any listeners in the process.

TrackGroup:removeFrom(listener: ZoneListener)

Removes this TrackGroup from the listener, making it no longer tracked for that listener.

TrackGroup:addItem(item: ZoneItem) -> ()

Tracks the items inside ZoneItem, updating any listeners in the process.

TrackGroup:addItems(item: ZoneItems) -> ()

Tracks the items inside ZoneItems, updating any listeners in the process.

TrackGroup:removeItem(item: ZoneItem) -> ()

Untracks the items inside ZoneItem, updating any listeners in the process.

TrackGroup:removeItems(items: ZoneItems) -> ()

Untracks the items inside ZoneItems, updating any listeners in the process.

TrackGroup:clear() -> ()

Removes all tracked items within this TrackGroup.

Fields


TrackGroup.mutable: ZoneItems

A mutable list of tracked items that have not yet been registered into listeners.

TrackGroup.immutable: Zoneitems

An immutable list of tracked items that have been registered into listeners.

TrackGroup.listeners:

All ZoneListeners currently tracking this TrackGroup.

Examples


Some simple examples to help you grasp how to use SimplerZone in common usecases.

--!strict
local Zone = require("@game/ReplicatedStorage/SimplerZone")
local Players = game:GetService("Players")

-- Build zone from all parts inside workspace.SafeZone
local shapeInstances = {}
for _, part in workspace.SafeZone:GetChildren() do
    assert(part:IsA("BasePart"))
    table.insert(shapeInstances, Zone.shapeInstance.fromPart(part))
end

local safeZone = Zone.new(shapeInstances)

local listener = Zone.listener.new({
    groups = { Zone.defaultGroups.players },
    zones = { safeZone },
    precision = "BoundingBox",
    queryShape = "Point",
})

listener:observe(function(item)
    local plr = item.player
    if not plr then return end

    -- Grant invincibility on enter
    plr:SetAttribute("InSafeZone", true)
    print(`[SafeZone] {plr.Name} is now protected`)

    return function()
        -- Revoke invincibility on exit
        plr:SetAttribute("InSafeZone", false)
        print(`[SafeZone] {plr.Name} left protection`)
    end
end)
--!strict
local Zone = require("@game/ReplicatedStorage/SimplerZone")

-- Build zone from all parts inside workspace.KillZone
local shapeInstances = {}
for _, part in workspace.KillZone:GetChildren() do
    assert(part:IsA("BasePart"))
    table.insert(shapeInstances, Zone.shapeInstance.fromPart(part))
end

local killZone = Zone.new(shapeInstances)

local listener = Zone.listener.new({
    groups = {Zone.defaultGroups.players},
    zones = {killZone},
    precision = "Part", -- If any part of the player touches the killzone it should register
    queryShape = "Box", -- Detect the whole part, not just the center
})

listener:onEnter(function(item)
    local plr = item.player
    local character = item.model
    if not plr or not character then return end

    -- Kill the player immediately on entry
    local humanoid = character:FindFirstChildWhichIsA("Humanoid")
    if humanoid then
        humanoid.Health = 0
        print(`[KillZone] {plr.Name} was eliminated`)
    end
end)
--!strict
local Zone = require("@game/ReplicatedStorage/SimplerZone")

local hitboxShape = Zone.shape.new("Box", CFrame.identity, vector.create(4, 6, 4))
local hitboxZone = Zone.new()

local listener = Zone.listener.new({
    groups = {Zone.defaultGroups.players},
    zones = {hitboxZone},
    precision = "BoundingBox",
    queryShape = "Point",
})

-- Tick damage while player overlaps the hitbox
listener:observe(function(item)
    local plr = item.player
    local character = item.model
    if not plr or not character then return end

    local humanoid = character:FindFirstChildWhichIsA("Humanoid")

    print(`[Hitbox] {plr.Name} entered {enemy.Name}'s attack range`)

    local connection = game:GetService("RunService").Heartbeat:Connect(function()
        if humanoid and humanoid.Health > 0 then
            humanoid:TakeDamage(damage)
        end
    end)

    return function()
        connection:Disconnect()
        print(`[Hitbox] {plr.Name} escaped {enemy.Name}'s attack range`)
    end
end)

-- Attach a damage hitbox to a single enemy NPC
local function attachEnemyHitbox(enemy: Model, damage: number)
    local rootAttach = assert(enemy:FindFirstChild("RootAttachment", true))

    local shapeInst = Zone.shapeInstance.new(hitboxShape)

    -- Keep the shape synced as the enemy moves
    shapeInst:syncWithAttachment(rootAttach)
    shapeInst:attach(hitboxZone)
    hitboxZone:rebuild()
end

-- Example: attach hitbox to every enemy in workspace.Enemies
for _, enemy in workspace.Enemies:GetChildren() do
    if enemy:IsA("Model") then
        attachEnemyHitbox(enemy, 5) -- 5 damage per heartbeat tick
    end
end