In short
A LocalScript binds a Punch action and fires a RemoteEvent with no arguments. A Script in ServerScriptService checks the attacker is alive and off cooldown, counts the combo, then calls workspace:GetPartBoundsInBox with a box a few studs in front of the attacker's root part, excluding their own character. Each Humanoid found takes damage once.
- The client sends no target and no damage. The server works both out, so a cheater cannot hit across the map.
- Collect Humanoids in a table so a target with 15 body parts in the box takes damage once, not 15 times.
- Prefer a box query to
Touchedon a fist: Touched misses fast swings and fires many times.
Get the script
Punch combo and hitbox server script
A Script in ServerScriptService: a 3-hit punch combo with a server cooldown, a box hitbox in front of the attacker, no friendly fire and a KOs stat.
-- Script in ServerScriptService named CombatService.
-- Punch combos: the client asks to punch, the server checks the cooldown, finds
-- targets in a box in front of the attacker and deals the damage.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DAMAGE = 10 -- per punch
local FINISHER_DAMAGE = 20 -- the last punch of a combo
local COMBO_LENGTH = 3
local PUNCH_COOLDOWN = 0.35 -- seconds between punches
local COMBO_RESET = 1 -- seconds without a punch before the combo starts over
local HITBOX_SIZE = Vector3.new(5, 5, 5)
local HITBOX_DISTANCE = 3.5 -- studs in front of the attacker's root
local punchEvent = Instance.new("RemoteEvent")
punchEvent.Name = "Punch"
punchEvent.Parent = ReplicatedStorage
type State = { last: number, combo: number }
local states: { [Player]: State } = {}
local function addKO(player: Player)
local leaderstats = player:FindFirstChild("leaderstats")
local kos = leaderstats and leaderstats:FindFirstChild("KOs")
if kos and kos:IsA("IntValue") then
kos.Value += 1
end
end
punchEvent.OnServerEvent:Connect(function(player: Player)
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = humanoid and humanoid.RootPart
if not (character and humanoid and root) or humanoid.Health <= 0 then
return
end
-- The server keeps its own clock, so a cheater cannot punch faster by firing more often
local now = os.clock()
local state: State = states[player] or { last = -math.huge, combo = 0 }
states[player] = state
if now - state.last < PUNCH_COOLDOWN then
return
end
state.combo = if now - state.last > COMBO_RESET then 1 else state.combo % COMBO_LENGTH + 1
state.last = now
local damage = if state.combo == COMBO_LENGTH then FINISHER_DAMAGE else DAMAGE
-- The hitbox: a box in front of where the server sees the attacker
local boxCFrame = root.CFrame * CFrame.new(0, 0, -HITBOX_DISTANCE)
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
local hitHumanoids: { [Humanoid]: boolean } = {}
for _, part in workspace:GetPartBoundsInBox(boxCFrame, HITBOX_SIZE, params) do
local model = part:FindFirstAncestorOfClass("Model")
local target = model and model:FindFirstChildOfClass("Humanoid")
local targetPlayer = if model then Players:GetPlayerFromCharacter(model) else nil
-- No friendly fire: skip players on the attacker's team
local sameTeam = targetPlayer and not player.Neutral and targetPlayer.Team == player.Team
-- One hit per Humanoid, however many of its parts are in the box
if target and target.Health > 0 and not sameTeam and not hitHumanoids[target] then
hitHumanoids[target] = true
target:TakeDamage(damage)
if target.Health <= 0 then
addKO(player)
end
end
end
end)
Players.PlayerAdded:Connect(function(player: Player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local kos = Instance.new("IntValue")
kos.Name = "KOs"
kos.Parent = leaderstats
leaderstats.Parent = player
end)
Players.PlayerRemoving:Connect(function(player: Player)
states[player] = nil
end)
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
ServerScriptService
CombatService Script new
StarterPlayer
StarterPlayerScripts
CombatClient LocalScript new
Settings you can change
DAMAGEIn the script- Damage of the first punches of a combo. Default:
10. FINISHER_DAMAGEIn the script- Damage of the last punch of a combo. Default:
20. PUNCH_COOLDOWNIn the script- Seconds between punches, kept by the server. Default:
0.35.
Use it with the CombatClient LocalScript from Step 2, which sends the punch on a click, a gamepad trigger or a phone button.
What a cheater can and cannot doThe server keeps the cooldown and builds the hitbox from where it sees the attacker, so firing the RemoteEvent faster or from far away does not help a cheater. A cheater who moves their character close to someone can still punch them, as any player can.
Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add a punch combat system: a LocalScript that fires a Punch RemoteEvent on click, gamepad R2 or a phone button, and a server Script with a 0.35 second cooldown, a 3-hit combo where the last hit does 20 damage, a 5 stud hitbox in front of the attacker using GetPartBoundsInBox, no friendly fire, and a KOs leaderstat.
On this page
How a server-side hitbox works
When the player presses Punch, their device sends a RemoteEvent with nothing in it. The server looks at its own copy of the attacker: are they alive, has the cooldown passed, which hit of the combo is this. Then it asks the physics engine which parts are inside a box in front of the attacker's root part. Any Humanoid those parts belong to, other than the attacker and their teammates, takes damage.
| Decision | Made by | Why |
|---|---|---|
| When to punch | Client | Input lives on the player's device. |
| Cooldown and combo | Server | A client could fire the event 100 times a second. |
| Who is hit | Server | A client could name any player as the target. |
| Damage | Server | A client could send any number. |
Step 1: The server script
Insert a Script in ServerScriptService named CombatService:
-- Script in ServerScriptService named CombatService.
-- Punch combos: the client asks to punch, the server checks the cooldown, finds
-- targets in a box in front of the attacker and deals the damage.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local DAMAGE = 10 -- per punch
local FINISHER_DAMAGE = 20 -- the last punch of a combo
local COMBO_LENGTH = 3
local PUNCH_COOLDOWN = 0.35 -- seconds between punches
local COMBO_RESET = 1 -- seconds without a punch before the combo starts over
local HITBOX_SIZE = Vector3.new(5, 5, 5)
local HITBOX_DISTANCE = 3.5 -- studs in front of the attacker's root
local punchEvent = Instance.new("RemoteEvent")
punchEvent.Name = "Punch"
punchEvent.Parent = ReplicatedStorage
type State = { last: number, combo: number }
local states: { [Player]: State } = {}
local function addKO(player: Player)
local leaderstats = player:FindFirstChild("leaderstats")
local kos = leaderstats and leaderstats:FindFirstChild("KOs")
if kos and kos:IsA("IntValue") then
kos.Value += 1
end
end
punchEvent.OnServerEvent:Connect(function(player: Player)
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = humanoid and humanoid.RootPart
if not (character and humanoid and root) or humanoid.Health <= 0 then
return
end
-- The server keeps its own clock, so a cheater cannot punch faster by firing more often
local now = os.clock()
local state: State = states[player] or { last = -math.huge, combo = 0 }
states[player] = state
if now - state.last < PUNCH_COOLDOWN then
return
end
state.combo = if now - state.last > COMBO_RESET then 1 else state.combo % COMBO_LENGTH + 1
state.last = now
local damage = if state.combo == COMBO_LENGTH then FINISHER_DAMAGE else DAMAGE
-- The hitbox: a box in front of where the server sees the attacker
local boxCFrame = root.CFrame * CFrame.new(0, 0, -HITBOX_DISTANCE)
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
local hitHumanoids: { [Humanoid]: boolean } = {}
for _, part in workspace:GetPartBoundsInBox(boxCFrame, HITBOX_SIZE, params) do
local model = part:FindFirstAncestorOfClass("Model")
local target = model and model:FindFirstChildOfClass("Humanoid")
local targetPlayer = if model then Players:GetPlayerFromCharacter(model) else nil
-- No friendly fire: skip players on the attacker's team
local sameTeam = targetPlayer and not player.Neutral and targetPlayer.Team == player.Team
-- One hit per Humanoid, however many of its parts are in the box
if target and target.Health > 0 and not sameTeam and not hitHumanoids[target] then
hitHumanoids[target] = true
target:TakeDamage(damage)
if target.Health <= 0 then
addKO(player)
end
end
end
end)
Players.PlayerAdded:Connect(function(player: Player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
local kos = Instance.new("IntValue")
kos.Name = "KOs"
kos.Parent = leaderstats
leaderstats.Parent = player
end)
Players.PlayerRemoving:Connect(function(player: Player)
states[player] = nil
end)
The combo counts 1, 2, 3, 1, 2, 3 while punches come within COMBO_RESET seconds of each other, and the third does FINISHER_DAMAGE. The hitbox is a HITBOX_SIZE box whose centre is HITBOX_DISTANCE studs in front of the root part: root.CFrame * CFrame.new(0, 0, -3.5) moves forward, because a part's front is its minus Z direction.
OverlapParams with Exclude and the attacker's character keeps the attacker out of their own hitbox. NPCs with a Humanoid can be hit too. When a hit takes a player to 0 health, the attacker gets a KO in the player list.
Step 2: The punch input
Insert a LocalScript in StarterPlayer > StarterPlayerScripts named CombatClient:
-- LocalScript in StarterPlayerScripts named CombatClient.
-- Click, tap the Punch button or press R2 on a gamepad to punch.
local ContextActionService = game:GetService("ContextActionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local punchEvent = ReplicatedStorage:WaitForChild("Punch") :: RemoteEvent
local function onPunch(_name: string, state: Enum.UserInputState, _input: InputObject): Enum.ContextActionResult
if state == Enum.UserInputState.Begin then
punchEvent:FireServer()
end
-- Pass, so clicks still reach buttons and tools
return Enum.ContextActionResult.Pass
end
ContextActionService:BindAction("Punch", onPunch, true, Enum.UserInputType.MouseButton1, Enum.KeyCode.ButtonR2)
ContextActionService:SetTitle("Punch", "Punch")
ContextActionService:BindAction binds one action to the left mouse button and the gamepad's R2, and its third argument, true, adds a Punch button on touch screens. It returns Pass so clicks still reach tools and buttons.
Step 3: Make it feel good
- Animations: play a different punch animation for each combo hit on the client, with the Animator in the character's Humanoid. The animation guide shows how to load and play one.
- Hit effects: have the server fire a second RemoteEvent to all clients with the hit position, and play a sound and a small particle burst there.
- Knockback: a target's character is simulated by the target's own device, so a push set by the server may be overwritten. Games usually ask the target's client to apply it, or use a short-lived constraint, and test it with two players.
- Blocking: add a Block action that sets an attribute on the player while held, and have the server halve damage when the target has it.
Test it
- Insert a dummy to punch: on the Avatar tab, open the Rig Builder and add a rig.
- Press Play, walk up to the dummy and click three times. Its health goes down by 10, 10, then 20.
- Click as fast as you can: the punches land at most every 0.35 seconds.
- Start a 2-player test and punch the other player until they fall: KOs goes to 1.
Common errors and fixes
If punches do nothing, check the Output window on both the Client and Server views.
Infinite yield possible on 'ReplicatedStorage:WaitForChild("Punch")'
CombatService did not run, so the RemoteEvent was never created. It must be a Script in ServerScriptService.
Punching does nothing to the dummy
Stand closer: the box reaches about 6 studs in front of you. Also check the dummy has a Humanoid with health above 0.
Teammates take no damage
That is the friendly fire check. Players on no team can always hit each other.
Clicking a button in my menu also punches
The Punch action is bound to the mouse button, which also clicks menus. Bind it to a key such as F instead, or call ContextActionService:UnbindAction("Punch") while a menu is open and bind it again when it closes.
Build it with RoCode
If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts in your Explorer.
Add battlegrounds-style combat: a 3-punch combo on click with a server hitbox in front of the player, a stronger third hit, a block on F that halves damage, and a KOs leaderboard.
- Writes the server combat script, the input LocalScript and the RemoteEvents
- Reads your place first to reuse your leaderstats and team setup
- Is told to compile-check each script with Check Script, and sets undo points around each batch
After a build, RoCode can run its test robot: it plays your game in Studio, clicks, presses keys and equips tools, and reports PASS or FAIL. It plays as one player and checks only what it chose to test, so test hits on a second player yourself. RoCode cannot make punch animations: create them in the Animation Editor. How RoCode connects to Studio.
Questions
How do I make a hitbox in Roblox?
Use workspace:GetPartBoundsInBox(cframe, size, params) on the server with a box in front of the attacker, and damage each Humanoid found once. Step 1 has the full script.
Should hitboxes be on the client or the server?
On the server for anything that deals damage. Some games detect hits on the client for responsiveness, but then the server must check every claimed hit, which is more work than this guide's approach.
Why does my punch hit the same player many times?
Each body part in the box is a separate result. Keep a table of Humanoids already hit in this punch, as the script does.
How do I count kills?
When a hit takes a Humanoid to 0 health, add 1 to the attacker's KOs value, as CombatService does.
Sources and further reading
- Roblox Creator Docs: WorldRoot:GetPartBoundsInBox. the box query
- Roblox Creator Docs: OverlapParams. excluding the attacker
- Roblox Creator Docs: ContextActionService. one action for mouse, gamepad and a phone button
- Roblox Creator Docs: Security tactics. checking what clients send
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.