§ How to · with Picoo

How to make a damage system in Roblox

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

Two lines of Luau reduce a player's health, and they are not interchangeable — one honours spawn protection and one walks straight through it.

Damage is where hitbox accuracy, server authority and spawn protection all meet, and every one of them has a common way of going wrong.

TakeDamage, not raw Health

Damage goes through Humanoid:TakeDamage so ForceFields and spawn protection actually protect.

Spatial-query hitboxes

GetPartBoundsInRadius instead of .Touched — no missed hits on fast swings, no duplicate hits.

Server-validated hits

The client says "I swung"; the server decides who was in range and how much it cost them.

Per-swing debounce

One attack damages each target once, however many parts it overlapped.

Team and self-damage rules

Friendly fire, self-hits and dead targets are filtered before any health changes.

Damage feedback

Hit sound, damage number and a brief highlight so a landed hit reads instantly.

Files Picoo ships for this prompt

2 files · 175 lines · ~30s · 1 credit

DamageService

Hit resolution, validation, ForceField-safe application, debounce.

120 lines

DamageFeedback

Damage numbers, hit sound, target flash.

55 lines

Sample output: ServerScriptService.DamageService

-- THE distinction: TakeDamage() checks for a ForceField and does nothing if one
-- exists. Writing Health directly ignores it — so raw Health kills players
-- through spawn protection, which reads as a bug nobody can reproduce on demand.
local function applyDamage(target: Humanoid, amount: number)
	if target.Health <= 0 then return end
	target:TakeDamage(amount)          -- respects ForceField
	-- target.Health -= amount          -- does NOT
end

-- Hitbox by spatial query, not .Touched. A fast swing can move the blade past a
-- character between frames and never raise Touched at all; the query samples the
-- volume instead of relying on a collision landing on a frame boundary.
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { attackerChar }

local function resolveSwing(attacker: Player, origin: CFrame)
	local hit: { [Humanoid]: boolean } = {}   -- one hit per target per swing
	for _, part in workspace:GetPartBoundsInRadius(origin.Position, RANGE, params) do
		local char = part:FindFirstAncestorOfClass("Model")
		local hum = char and char:FindFirstChildOfClass("Humanoid")
		if hum and not hit[hum] and hum.Health > 0 then
			hit[hum] = true
			applyDamage(hum, DAMAGE)
		end
	end
end

Building a damage system in Roblox

Damage looks like one line of code, and that is why it goes wrong so often.

Start with the line itself. Humanoid:TakeDamage(amount) and Humanoid.Health -= amount appear equivalent, and for an unprotected target they are. The difference is the ForceField: TakeDamage checks for one and does nothing when it is there, while writing Health straight through ignores it. If your spawn has protection and players still die inside it, that is the entire bug — and it is miserable to track down, because it only shows up in the few seconds after a respawn.

The hitbox is the next layer. .Touched is the obvious choice and the wrong one for anything fast. It only fires when two parts are found overlapping on a simulated frame, so a blade swinging quickly can pass clean through a character between frames and register nothing. A spatial query at the moment of the swing — GetPartBoundsInRadius around the blade, or a shapecast along its arc — asks what is in the volume rather than hoping a collision happened to land. The same change fixes duplicate hits, because you get a list you can deduplicate rather than a stream of events.

Then authority. The client knows it swung and where it was facing; that is all it should be telling the server. The moment the amount of damage travels over a RemoteEvent, that remote becomes a kill button for anyone who inspects it. Send the intent, validate the range on the server, and let the server supply the number from the weapon's own configuration.

Feedback is not decoration here. A hit that produces no sound, no number and no flash reads as a miss, and players compensate by attacking more, which makes tuning feel wrong even when the math is right.

See more on the Luau generator, the game builder, or browse the full blog.

Frequently asked

TakeDamage or Humanoid.Health -= amount?+

TakeDamage, in almost every case. It checks for a ForceField on the character and does nothing when one is present, which is exactly what spawn protection is. Subtracting from Health ignores ForceFields entirely — players get killed inside their spawn shield, and the bug is nearly impossible to reproduce deliberately. Write Health directly only when you intend to bypass protection, such as a scripted cutscene death.

Why do my sword hits miss?+

Because .Touched depends on a collision happening on a frame Roblox actually simulates. A blade swinging at speed can be on one side of a character in one frame and the other side in the next, touching nothing in between. A spatial query — GetPartBoundsInRadius or a shapecast along the swing — samples the volume instead of waiting for a collision to land.

Why does one swing deal damage five times?+

The hitbox overlapped five parts of the same character. Track which Humanoids you have already hit within the swing and skip repeats — a table keyed by Humanoid, discarded when the swing ends.

Can the client apply damage?+

Health changed on the client does not replicate, so it does nothing visible to anyone else. Sending the damage amount to the server through a RemoteEvent and applying whatever arrives is worse — that is a remote that lets any client kill any player. The client sends intent ("I attacked, facing here"); the server checks range and applies the number it already knows.

How do I stop friendly fire?+

Filter before applying: same Team, same player, or an already-dead Humanoid all return early. Doing this inside applyDamage rather than at each call site means a new weapon cannot forget the rule.

Related Picoo prompts