Hitbox
Hitbox wraps an invisible, non-colliding Part and uses Workspace:GetPartsInPart to find
what is overlapping it. Use it for melee swings, area-of-effect abilities, trigger zones or
anything else where you want to know which parts or humanoids are inside a volume right now,
or for a period of time.
A Hitbox can be queried once (Check, CheckHumanoids), scanned for a fixed duration on
Heartbeat (Scan, ScanHumanoids) or scanned on your own schedule using a repeat interval
or any signal (ScanWithEvent, ScanHumanoidsWithEvent). The hitbox part can be welded to
another part so it follows a character or weapon. Everything the hitbox creates (the part,
welds, timers and connections) is tracked by a Trove
and released by Destroy; destroying the hitbox part also destroys the hitbox.
Create one with Hitbox.new, or from existing geometry with Hitbox.fromPart,
Hitbox.fromModel or Hitbox.convert.
local Hitbox = require(path.to.Hitbox)
local function swing(character: Model)
local rootPart = character:WaitForChild("HumanoidRootPart") :: BasePart
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- A 4x4x4 block one stud in front of the character, welded to the root so it follows them
local hitbox = Hitbox.new(
rootPart.CFrame * CFrame.new(0, 0, -3),
Vector3.new(4, 4, 4),
Enum.PartType.Block,
params,
workspace,
false,
rootPart
)
-- Scan every frame for half a second and damage each humanoid once
local alreadyHit: { [Humanoid]: boolean } = {}
hitbox:ScanHumanoids(0.5, function(humanoid)
if alreadyHit[humanoid] then
return
end
alreadyHit[humanoid] = true
humanoid:TakeDamage(10)
end)
task.delay(0.5, function()
hitbox:Destroy()
end)
end
Credits: Trove and Timer are by sleitnick (sleitnick's RbxUtil). Wally installs both.
Installation and guide: Hitbox package page.
Types
ConnectionLike
interface ConnectionLike {Connected: boolean--
Whether the connection is still active.
}
The object returned by every Scan* method. It is the connection returned by the signal (or
internal Timer) that drives the scan; call Disconnect to stop scanning early. The hitbox's
Trove also disconnects it when the hitbox is destroyed.
SignalLike
interface SignalLike {Connect: (callback: (...any) → ...any) → ConnectionLike--
Connects a callback and returns its connection.
}
Anything with a Connect method can drive a scan: RBXScriptSignals such as
RunService.Heartbeat, or custom signals like sleitnick's Signal. The arguments the signal
fires with are ignored; each firing simply triggers one check.
PredicateFunc
Filter applied to each overlapping part. Return true to keep the part. The predicate is
called inside pcall, so a part is also dropped if the predicate errors.
DisconnectPredicateFunc
type DisconnectPredicateFunc = () → boolean
Called before every check of an event-driven scan. Return true to keep scanning; returning
false disconnects the scan instead of performing that check. Scan and ScanHumanoids use
one of these internally to stop after their duration.
BasePartCallback
Receives one overlapping part. Called in its own thread (task.spawn) for every part found on
every check, so a part that stays inside the hitbox is reported once per check.
HumanoidCallback
Receives one living humanoid. Called in its own thread (task.spawn) for every humanoid found
on every check, so the same humanoid is reported repeatedly while it stays inside the hitbox.
Properties
Trove
This item is read only and cannot be modified. Read OnlyHitbox.Trove: Trove
The Trove that owns everything the hitbox
creates: the hitbox part, the weld, scan timers and scan connections. Destroy destroys it.
You can add your own objects to it so they are cleaned up with the hitbox.
HitboxPart
This item is read only and cannot be modified. Read OnlyHitbox.HitboxPart: Part
The physical part used for Workspace:GetPartsInPart. It is anchored (until welded), has
CanCollide, CastShadow off and Massless on. Move it with CFrame/PivotTo if you want to
reposition an unwelded hitbox. The Trove is attached to this instance, so destroying the part
destroys the hitbox.
HitboxParams
Hitbox.HitboxParams: OverlapParams
The OverlapParams passed to every GetPartsInPart call. Change its filter list at any time
to include or exclude instances from future checks.
Weld
This item is read only and cannot be modified. Read OnlyHitbox.Weld: WeldConstraint?
The WeldConstraint created by WeldTo, or nil if the hitbox is not welded to anything.
It is parented to the part the hitbox was welded to.
Functions
new
ConstructorHitbox.new(shape: Enum.PartType,--
Shape of the hitbox part (Block, Ball, Cylinder, ...).
hitboxParams: OverlapParams,--
Filter used for every GetPartsInPart query; stored as HitboxParams.
visible: boolean?,--
Show the part as red and half transparent instead of invisible.
) → Hitbox--
The new hitbox.
Creates a hitbox by building a new part from a position, size and shape.
The part is anchored, non-colliding, massless and casts no shadow. When visible is true it is
drawn as a red, half-transparent part (handy while tuning a hitbox); otherwise it is fully
invisible. When weldTo is given, WeldTo is called immediately so the hitbox part follows
that part and keeps the offset described by cframe.
CAUTION
parent defaults to game (the DataModel) when omitted. Pass workspace or a folder inside
it so the part is actually in the world.
Errors
| Type | Description |
|---|---|
| "CFrame must be a CFrame" | `cframe` is not a CFrame. |
| "Size must be a Vector3" | `size` is not a Vector3. |
| "Shape must be an Enum.PartType item" | `shape` is not an EnumItem. |
| "HitboxParams must be an OverlapParams" | `hitboxParams` is not an OverlapParams. |
fromPart
ConstructorHitbox.fromPart(hitboxParams: OverlapParams,--
Filter used for every GetPartsInPart query.
visible: boolean?,--
Show the part as red and half transparent instead of invisible.
) → Hitbox--
The new hitbox.
Creates a hitbox that copies the CFrame, Size and shape of an existing part. The shape is
only copied when part is a Part; other BaseParts (meshes, unions, wedges) produce a
Block. The original part is left untouched; use convert if you want it removed.
The parent, visible and weldTo arguments behave exactly as in new.
fromModel
ConstructorHitbox.fromModel(hitboxParams: OverlapParams,--
Filter used for every GetPartsInPart query.
visible: boolean?,--
Show the part as red and half transparent instead of invisible.
) → Hitbox--
The new hitbox.
Creates a Block hitbox that covers a model's bounding box, using Model:GetBoundingBox()
for the CFrame and Model:GetExtentsSize() for the size. The model itself is left untouched;
use convert if you want it removed.
The parent, visible and weldTo arguments behave exactly as in new.
convert
ConstructorHitbox.convert(instanceToConvert: Model | BasePart,--
The part or model to turn into a hitbox; it is destroyed afterwards.
hitboxParams: OverlapParams,--
Filter used for every GetPartsInPart query.
visible: boolean?,--
Show the part as red and half transparent instead of invisible.
) → Hitbox--
The new hitbox.
Replaces a placeholder part or model with a hitbox. Behaves like fromPart for a BasePart
and fromModel for a Model, then destroys the original instance. Useful when you lay
out hitbox volumes in Studio and want them swapped for real hitboxes at runtime.
The parent, visible and weldTo arguments behave exactly as in new.
Errors
| Type | Description |
|---|---|
| "Unable to convert class ..." | `instanceToConvert` is neither a `BasePart` nor a `Model`. |
Check
Performs one Workspace:GetPartsInPart(HitboxPart, HitboxParams) query and returns the
overlapping parts. When a predicate is given, only parts it returns true for are kept; the
predicate is wrapped in pcall, so a part is also dropped if the predicate errors.
This is the raw building block. Use CheckHumanoids when you care about characters rather
than individual parts, and the Scan* methods to run checks repeatedly.
local parts = hitbox:Check(function(part)
return part:HasTag("Breakable")
end)
CheckHumanoids
Hitbox:CheckHumanoids() → {Humanoid}--
Living humanoids whose character parts are inside the hitbox.
Performs one check and resolves the overlapping parts to living humanoids. For each part found
by Check, the nearest Model ancestor is looked up and the first Humanoid descendant of
that model with Health > 0 is collected. Dead humanoids and parts that are not inside a model
with a humanoid are ignored.
CAUTION
The list is not de-duplicated: a character with several parts inside the hitbox appears once per part. Track humanoids you have already handled if you need one hit per character.
ScanWithEvent
Hitbox:ScanWithEvent(disconnectPredicate: DisconnectPredicateFunc?--
Return false to stop the scan before the next check.
) → ConnectionLike--
Connection driving the scan; Disconnect it to stop early.
Repeatedly runs Check(predicate) and calls callback for every part found, driven either by
a fixed interval or by a signal:
-
If
intervalis a number, a Timer is constructed inside the hitbox's Trove with that many seconds between ticks, started, and itsTicksignal drives the checks. -
If
intervalis a signal (RunService.Heartbeat, a custom Signal, ...), one check runs every time the signal fires. The signal's arguments are ignored.
Before each check disconnectPredicate is called (if given); when it returns false the scan
disconnects itself instead of checking. Scan uses this to stop after a duration. Without a
disconnect predicate the scan runs until you call Disconnect on the returned connection or
destroy the hitbox.
Each callback is spawned in its own thread, and a part that stays inside the hitbox is reported on every check, so de-duplicate in the callback if you only want to react once per part.
-- Scan every 0.1s while the ability is active
local connection = hitbox:ScanWithEvent(0.1, function(part)
print("Touching", part:GetFullName())
end, nil, function()
return ability.Active
end)
Scan
Hitbox:Scan(duration: number,--
How many seconds to keep scanning.
) → ConnectionLike--
Connection driving the scan; Disconnect it to stop early.
Scans for parts on every RunService.Heartbeat for duration seconds, then stops on its own.
This is ScanWithEvent(RunService.Heartbeat, callback, predicate, ...) with a disconnect
predicate that compares os.clock() against the start time. The callback runs for every part
on every frame it is inside the hitbox.
Returns immediately; the scan happens in the background. Use ScanWithEvent if you need a
slower interval or a different signal.
ScanHumanoidsWithEvent
Hitbox:ScanHumanoidsWithEvent(disconnectPredicate: DisconnectPredicateFunc?--
Return false to stop the scan before the next check.
) → ConnectionLike--
Connection driving the scan; Disconnect it to stop early.
Humanoid version of ScanWithEvent: repeatedly runs CheckHumanoids and calls callback for
every living humanoid found. interval is either a number of seconds (an internal Timer is
created and started in the Trove) or a signal that triggers a check each time it fires.
disconnectPredicate is called before each check and stops the scan when it returns false.
Each callback is spawned in its own thread. Because CheckHumanoids does not de-duplicate,
the same humanoid can be reported several times per check and again on every following check
while it stays inside the hitbox.
ScanHumanoids
Hitbox:ScanHumanoids(duration: number,--
How many seconds to keep scanning.
) → ConnectionLike--
Connection driving the scan; Disconnect it to stop early.
Scans for living humanoids on every RunService.Heartbeat for duration seconds, then stops
on its own. Equivalent to ScanHumanoidsWithEvent(RunService.Heartbeat, callback, ...) with a
time-based disconnect predicate. Returns immediately; the scan runs in the background.
local hit = {}
hitbox:ScanHumanoids(0.4, function(humanoid)
if not hit[humanoid] then
hit[humanoid] = true
humanoid:TakeDamage(25)
end
end)
WeldTo
Hitbox:WeldTo() → ()
Attaches the hitbox part to part with a WeldConstraint so it moves with it, keeping the
current relative offset between the two. The hitbox part is unanchored, and the constraint is
parented to part (named after the hitbox part) and stored in Weld. Any previous weld is
destroyed first, so calling this again re-parents the hitbox to a different part.
The weld is added to the Trove and removed when the hitbox is destroyed. Position the hitbox
part where you want it relative to part before welding.
Destroy
Hitbox:Destroy() → ()
Destroys the hitbox: the Trove is destroyed, which removes the hitbox part and weld, stops any
running scan timers and disconnects every scan connection. Always call this (or destroy
HitboxPart, which has the same effect) when you are done, otherwise the part and its scans
keep running.