The site is under maintenance, so logging in is temporarily unavailable. All resources and the MCP still work.

AI Skills

Zones and hitboxes

By Roblox Vibecoders · roblox-zones-hitboxes

Use when detecting players, characters or parts inside an area in Roblox - safe zones, shops, music or capture areas with ZonePlus (Zone.new, playerEntered/playerExited), or attack, explosion and ability hitboxes with kashtheking/hitbox (GetPartsInPart, Scan, ScanHumanoids). Also use instead of Touched-based area checks.

Install it

Terminal
mkdir -p ~/.claude/skills/roblox-zones-hitboxes && curl -fsSL https://robloxvibecoders.com/skills/roblox-zones-hitboxes/SKILL.md -o ~/.claude/skills/roblox-zones-hitboxes/SKILL.md

Skills in ~/.claude/skills work in every project. Put them in your project's .claude/skills folder to share them with your team.

What your AI reads

SKILL.md
---
name: roblox-zones-hitboxes
description: Use when detecting players, characters or parts inside an area in Roblox - safe zones, shops, music or capture areas with ZonePlus (Zone.new, playerEntered/playerExited), or attack, explosion and ability hitboxes with kashtheking/hitbox (GetPartsInPart, Scan, ScanHumanoids). Also use instead of Touched-based area checks.
---

# Zones and hitboxes

Two different jobs:

- **Zones** are long-lived areas you ask "who is inside, who entered, who left" (safe zone, shop counter, music
  region, capture point). Use **ZonePlus**.
- **Hitboxes** are short-lived volumes you scan for a moment (sword swing, punch, explosion, dash). Use
  **kashtheking/hitbox**.

For a single one-off question with no object, call the spatial query directly:
`workspace:GetPartBoundsInBox(cframe, size, overlapParams)`, `workspace:GetPartBoundsInRadius(position, radius,
overlapParams)` or `workspace:GetPartsInPart(part, overlapParams)`.

Don't build areas on `Touched`/`TouchEnded`: they fire per body part and flicker as limbs move.

## ZonePlus

By 1ForeverHD. Version 3.2.0 is the latest release. It is **not** published to Wally by its author (only forks are),
so install it one of the documented ways:

- Creator Store model: `https://www.roblox.com/library/6245329519/ZonePlus`, or
- `ZonePlus.rbxm` from `https://github.com/1ForeverHD/ZonePlus/releases/latest`.

Place the `Zone` ModuleScript in `ReplicatedStorage` so server and client can require it.

```luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Zone = require(ReplicatedStorage.Zone)

-- Container: a Folder/Model of BaseParts, a single BasePart, or an array of BaseParts
local safeZone = Zone.new(workspace.Zones.Spawn)

safeZone.playerEntered:Connect(function(player: Player)
	player:SetAttribute("InSafeZone", true)
end)

safeZone.playerExited:Connect(function(player: Player)
	player:SetAttribute("InSafeZone", false)
end)
```

Client-only effects (music, lighting, UI prompts) use the local player events:

```luau
local beach = Zone.new(workspace.Zones.Beach)
beach.localPlayerEntered:Connect(function()
	beachMusic:Play()
end)
beach.localPlayerExited:Connect(function()
	beachMusic:Stop()
end)
```

API (from the ZonePlus docs and source):

| Member | Notes |
|---|---|
| `Zone.new(container)`, `Zone.fromRegion(cframe, size)` | constructors |
| `playerEntered`, `playerExited` | `(player)` |
| `localPlayerEntered`, `localPlayerExited` | client only, no arguments |
| `itemEntered`, `itemExited` | `(item)`, for things added with `zone:trackItem(characterOrBasePart)` (NPCs) |
| `partEntered`, `partExited` | `(part)`, unanchored parts only |
| `zone:findPlayer(player)`, `zone:getPlayers()` | instant checks |
| `zone:findPoint(position)` | returns `isInside, touchingZoneParts` |
| `zone:getRandomPoint()` | random position inside the zone |
| `zone:setAccuracy(name)` | `"Low"` 1s, `"Medium"` 0.5s, `"High"` 0.1s (default), `"Precise"` every frame |
| `zone:setDetection(name)` | `"Centre"` (default, HumanoidRootPart) or `"WholeBody"` |
| `zone:relocate()` | moves the zone parts out of Workspace while keeping it working |
| `zone:destroy()` | disconnects everything |

Zone part rules from the docs: start the parts in Workspace, keep them in the **Default** collision group, and prefer
plain Block parts (cheaper than other shapes). Make them Anchored, CanCollide false and Transparency 1. Checks only
run while something is connected to an event, so create each zone once and keep it.

## kashtheking/hitbox

KashTheKing's object-oriented hitbox: an invisible Part scanned with `workspace:GetPartsInPart`, with Trove cleanup
and sleitnick's Timer for interval scans.

```toml
[dependencies]
Hitbox = "kashtheking/hitbox@1.0.3" # installs sleitnick/trove and sleitnick/timer
```

| Member | Notes |
|---|---|
| `Hitbox.new(cframe, size, shape, overlapParams, parent?, visible?, weldTo?)` | `shape` is an `Enum.PartType` |
| `Hitbox.fromPart(part, overlapParams, parent?, visible?, weldTo?)` | copies the part's CFrame, size and shape |
| `Hitbox.fromModel(model, overlapParams, ...)` | bounding box of the model, Block shape |
| `Hitbox.convert(partOrModel, overlapParams, ...)` | like `fromPart`/`fromModel`, then destroys the original |
| `hitbox:Check(predicate?)` | one query now, returns `{ BasePart }` |
| `hitbox:CheckHumanoids()` | living Humanoids found, one entry **per overlapping part** |
| `hitbox:Scan(duration, callback, predicate?)` | runs `callback(part)` every Heartbeat for `duration` seconds |
| `hitbox:ScanHumanoids(duration, callback)` | same, `callback(humanoid)` |
| `hitbox:ScanWithEvent(intervalOrSignal, callback, predicate?, keepGoing?)` | number = seconds between scans; or any signal. Stops when `keepGoing()` returns false |
| `hitbox:ScanHumanoidsWithEvent(intervalOrSignal, callback, keepGoing?)` | humanoid version |
| `hitbox:WeldTo(part)` | unanchors the hitbox part and welds it so it follows |
| `hitbox:Destroy()` | removes the part, weld, timers and connections |
| `hitbox.HitboxPart` | the Part itself |

Scans return a connection you can `:Disconnect()`, but the part stays until `:Destroy()`.

### A melee swing (server)

```luau
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Hitbox = require(ReplicatedStorage.Packages.Hitbox)

local function targetParams(attacker: Model): OverlapParams
	local targets = {}
	for _, player in Players:GetPlayers() do
		if player.Character and player.Character ~= attacker then
			table.insert(targets, player.Character)
		end
	end
	local params = OverlapParams.new()
	params.FilterType = Enum.RaycastFilterType.Include -- only characters can be found
	params.FilterDescendantsInstances = targets
	return params
end

local function swing(attacker: Model, damage: number)
	local root = attacker:FindFirstChild("HumanoidRootPart") :: BasePart?
	if not root then
		return
	end

	local hitbox = Hitbox.new(
		root.CFrame * CFrame.new(0, 0, -3), -- 3 studs in front
		Vector3.new(5, 5, 5),
		Enum.PartType.Block,
		targetParams(attacker),
		workspace.Hitboxes, -- a Folder you created in Workspace
		false, -- set true while tuning to see it
		root -- follow the attacker
	)

	local alreadyHit: { [Humanoid]: true } = {}
	hitbox:ScanHumanoids(0.2, function(humanoid: Humanoid)
		if alreadyHit[humanoid] then
			return
		end
		alreadyHit[humanoid] = true
		humanoid:TakeDamage(damage)
	end)

	task.delay(0.3, function()
		hitbox:Destroy()
	end)
end
```

Add NPC models to `targets` if NPCs can be hit. Run damage hitboxes on the **server**. If you add client hitboxes for
responsiveness, the server must still re-check distance, cooldown and line of sight before applying damage.

## Mistakes to avoid

- **Not deduping hits.** Scans fire every tick and `CheckHumanoids` returns one Humanoid per overlapping body part.
  Keep a hit set per swing, as above.
- **Exclude filters that let the map in.** `CheckHumanoids` takes each part's nearest `Model` ancestor. For Terrain that
  ancestor is Workspace itself, so it can return an unrelated player's Humanoid. Use an Include filter of characters.
- **Relying only on the `predicate` argument.** In 1.0.3, `Check` removes rejected parts while iterating the same
  array, so a rejected part right after another rejected one can slip through. Filter with OverlapParams and re-check
  in the callback.
- **Omitting `parent`.** Always pass a folder in Workspace; the source falls back to parenting the part to `game`.
- **Forgetting `:Destroy()`.** Every hitbox you create is a Part in Workspace until destroyed.
- **Rewards or damage from client zone events.** `localPlayerEntered` is for effects. Gameplay decisions use the
  server's `playerEntered`.
- **Zone parts that collide, are unanchored or sit in a custom collision group.** They must be in the Default group.
- **Creating zones repeatedly** (per player, per frame, per round) instead of once at startup.
- **Expecting `partEntered` for anchored parts.** It only tracks unanchored parts. Use `trackItem` for NPC models.