Reliable zones using pure math & boolean logic

Hello, I am here presenting a small module that functions similarly to other zone modules. The key difference is that it doesn’t use Touched, GetPartsInPart, or any other form of spatial queries. It uses simple math and boolean logic for reliability.

This means you can have zones which have non-existent hitboxes that act on things that also don’t exist.

You can find it here: Zones - Creator Store

Zones can be any Part shape, and infinite bound checks are supported for all of them. You create them with either a supplied Part, or a table that has a CFrame, Size, and Shape.

The Zone will still work if the supplied Part/table changes any of its properties, allowing for dynamically changing zones.

Currently there are 2 zone types:

positionZone(ZoneHitbox)

This zone type requires you to supply targets for it to “watch”. The only requirement for something to be a target is that it has a “Position” or “CFrame.Position” property as a Vector3.

Meaning Cameras and Lua tables that fit the requirement are both valid targets that you can add to the Targets table.


playerZone(ZoneHitbox)

This zone type uses the position of players HumanoidRootPart to determine when they enter/exit the zone.


How to use

For each zone constructor, ZoneHitbox is the occupied area of the zone. It can be either a Part or a table with the properties CFrame, Size, and Shape like how a Part would have:

type ZoneHitbox={ CFrame:CFrame, Size:Vector3, Shape:Enum.PartType }

If you update the original ZoneHitbox object after creating the zone, the zone will dynamically update because it has a reference to the table/object. You can change any of the properties at any time.

Zones.positionZone(ZoneHitbox)

This creates a zone which “watches” a table of Targets that you supply. Anything that has a property Position or CFrame.Position as a Vector3 is supported as a target in that table.

For example, this is valid because tbl has a Position property as a Vector3 and the zone has a reference to tbl. (you can just use the camera itself as a target, this is just an example):

local Zones = require(game:GetService("ReplicatedStorage"):WaitForChild("Zones"))

local Part = workspace:WaitForChild("Part")

local cameraZone = Zones.positionZone(workspace.Part)


local tbl = {
	Position = Vector3.new()
}


game:GetService("RunService"):BindToRenderStep("DSDASD", Enum.RenderPriority.Camera.Value + 25, function()

	tbl.Position = workspace.CurrentCamera.CFrame.Position
end)


cameraZone.Targets = { tbl }


cameraZone.Entered = function(trg)

	print(trg, " entered the zone")
end


cameraZone.Exited = function(trg)

	print(trg, " exited the zone")
end

When using a positionZone, the parameter passed to the callbacks Entered/Exited will be the original target reference that did so, as you can see in the code above.


Zones.playerZone(ZoneHitbox)

This one is self-explanatory, it uses the HumanoidRootPart position of each player to detect when a player enters/exits the zone.

When using a playerZone, the parameter passed to the Entered/Exited callbacks is the Player instance that did so. See “Example Code” for an example on this zone type.


Regardless of whichever Zone type you use there is a property called FilterAxis. This property essentially turns the zone into an infinite boundary along the supplied local axis, meaning the “infinite zone” rotates with the zones’ CFrame.

For example, if you have a cylinder that you want to act as an infinitely tall circular zone, you will set FilterAxis to “X” because the cylinder is circular when looked at from its local X axis.

If you want to know which FilterAxis to use for your use-case, select the part in Studio, select the Move tool, then enter Local transformation space if you aren’t there already (you will see a little blue L next to the object):

Red - local X axis
Blue - local Z axis
Green - local Y axis

The image above shows why using FilterAxis = "X" on a cylinder creates an infinite circular zone that rotates with the cylinder.


Example code These are some examples on how to use the module.
Camera zone (LocalScript)
local Zones = require(game:GetService("ReplicatedStorage"):WaitForChild("Zones"))

local cameraZone = Zones.positionZone(workspace:WaitForChild("Part"))

cameraZone.Targets = { workspace.CurrentCamera }


cameraZone.Entered = function()

	print("camera entered the zone")
end


cameraZone.Exited = function()

	print("camera exited the zone")
end

Player zone infinite local Y axis
local Zones = require(game:GetService('ReplicatedStorage').Zones)

local PlayerZone = Zones.playerZone(workspace.Part)

PlayerZone.FilterAxis = "Y"


PlayerZone.Entered = function(plr)
	
	print(plr.Name .. " has entered the zone")
end


PlayerZone.Exited = function(plr)
	
	print(plr.Name .. " has exited the zone")
end

Position zone with dynamic tagged targets
local Zones = require(game:GetService("ReplicatedStorage").Zones)

local CollectionService = game:GetService("CollectionService")


local taggedZone = Zones.positionZone(workspace:WaitForChild("Part"))

taggedZone.Targets = CollectionService:GetTagged("SomeTag")


CollectionService:GetInstanceAddedSignal("SomeTag"):Connect(function(tagged)

	table.insert(taggedZone.Targets, tagged)
end)

CollectionService:GetInstanceRemovedSignal("SomeTag"):Connect(function(tagged)

	local found = table.find(taggedZone.Targets, tagged)

	if not found then return end

	table.remove(taggedZone.Targets, found)
end)



taggedZone.Entered = function(taggedItem)

	print(taggedItem.Name .. " entered the zone")
end


taggedZone.Exited = function(taggedItem)

	print(taggedItem.Name .. " exited the zone")
end

Use zone:Destroy() when you don’t need the zones anymore.

I hope this helps someone!

16 Likes

Super!
The module fills a real niche.
Please tighten the documentation.

1 Like

I just updated the thread with a “How to use” section. Let me know if there is anything I should add to it

Believe it or not I have an extremely specific use case for this, so thanks!

1 Like