Tag game hitbox

Hi im making a tag game and i wanted it so that when the player touches another player they get tagged (unlike untitled tag game where u have to click to tag another player). So I just wanted to know what was the best and most optimal way for the hitbox detection (in my case).

What ive tried:

  • raycast hitbox v4: I tried this but I had to have the .HitStart function running the whole game and considering that there will be multiple players it would be very un-optimized.

  • Region3: same thing as raycasting where I have to keep running the same function every frame which would very un-optimised again.

  • .Touched: super inconsistent and doesnt always fire when the event should fire.

None of these methods r suitable for my game.

What r some other methods that will work in my game without being un-optimised (also considering that I am a beginner).

2 Likes

I think in a situation like this I would just simply turn it into Touched Event.
I could be wrong but i’d think it’s the most simple and optimal method for detecting when one player touches another.
A code example would probably look something like this:

---Server Script local Players = game:GetService("Players") Players.PlayerAdded:Connect(function(player) player.CharacterAdded:Connect(function(character) local rootPart = character:WaitForChild("HumanoidRootPart") -- Add Touched event to the player's hitbox rootPart.Touched:Connect(function(hit) local taggedPlayer = Players:GetPlayerFromCharacter(hit.Parent) -- Ensure the object is another player's character if taggedPlayer and taggedPlayer ~= player then print(player.Name .. " tagged " .. taggedPlayer.Name) -- Perform your tagging logic here -- Example: Update scores or change roles end end) end) end) 

I would probably recommend aiming for an enhancement though and not just leaving it at that,
For example,
To improve the gameplay experience or prevent false positives:

  1. Add a Cooldown:
  • Prevent multiple tags from being registered immediately:
local cooldown = {} rootPart.Touched:Connect(function(hit) local taggedPlayer = Players:GetPlayerFromCharacter(hit.Parent) if taggedPlayer and taggedPlayer ~= player and not cooldown[taggedPlayer] then cooldown[taggedPlayer] = true print(player.Name .. " tagged " .. taggedPlayer.Name) -- Reset cooldown after 1 second task.delay(1, function() cooldown[taggedPlayer] = nil end) end end) 

Ensure Valid Collisions:

  • Check the distance between players to ensure it’s a valid tag:
if (rootPart.Position - hit.Position).Magnitude < 5 then print(player.Name .. " tagged " .. taggedPlayer.Name) end 

Use a Tagging Role:

  • Ensure only specific players (e.g., the “it” player) can tag others:
if player.Team.Name == "It" then print(player.Name .. " tagged " .. taggedPlayer.Name) end 

Sorry hindsight that seems like maybe more insight than requested. I’m kind of new to trying to help others and got carried away, But I hope this helps.

2 Likes

Thanks but I forgot to mention that ive tried using the touched event previously. I found out that player ping takes a big effect (even if u run it locally). Also the event doesnt always fire and overall the event is just so inconsistent. I dont think adding enhancement will stop the event from being inconsistent as the event is fired first. I appreciate u trying to help tho!

We could try this XD


Revised Solution: Distance-Based Detection

Instead of relying on the Touched event, we can periodically check the distance between players to determine when one tags another. This method avoids the issues with network latency and the inconsistency of Touched.


Server Script
local Players = game:GetService("Players") local RunService = game:GetService("RunService") -- Tagging settings local tagRange = 5 -- The distance required to tag another player local tagCooldown = 1 -- Cooldown between tags (in seconds) local taggedPlayers = {} -- To track cooldowns for tagged players -- Function to handle tagging local function handleTagging() local allPlayers = Players:GetPlayers() for _, player in ipairs(allPlayers) do local character = player.Character local humanoidRootPart = character and character:FindFirstChild("HumanoidRootPart") if humanoidRootPart then for _, otherPlayer in ipairs(allPlayers) do if otherPlayer ~= player then local otherCharacter = otherPlayer.Character local otherHumanoidRootPart = otherCharacter and otherCharacter:FindFirstChild("HumanoidRootPart") if otherHumanoidRootPart then local distance = (humanoidRootPart.Position - otherHumanoidRootPart.Position).Magnitude if distance <= tagRange and not taggedPlayers[otherPlayer.UserId] then -- Tag the player print(player.Name .. " tagged " .. otherPlayer.Name) -- Perform tag logic (e.g., update roles, scores) -- Add a cooldown for the tagged player taggedPlayers[otherPlayer.UserId] = true task.delay(tagCooldown, function() taggedPlayers[otherPlayer.UserId] = nil end) end end end end end end end -- Run the tagging check periodically RunService.Heartbeat:Connect(function() handleTagging() end) 

How This Works/Should work

  1. Distance-Based Tagging:
  • The script calculates the distance between players’ HumanoidRootPart objects using Magnitude.
  • If the distance is less than or equal to the tagRange, the tag is registered.
  1. Cooldown System:
  • A taggedPlayers table tracks recently tagged players and prevents them from being tagged repeatedly within a short time.
  1. Heartbeat Loop:
  • The RunService.Heartbeat loop runs at the frame rate but avoids excessive processing by only checking the players periodically.

I could be wrong but

Why This is Better

  1. Ping-Independent:
  • Distance checks are calculated on the server, ensuring that lag or ping doesn’t affect the detection.
  1. Consistent Behavior:
  • Unlike Touched, which relies on physics and collision events, this method uses deterministic distance calculations, making it far more reliable.
  1. Customizable:
  • You can adjust the tagRange and tagCooldown to fit your game’s design and balance.
  1. Lightweight:
  • Instead of running complex frame-by-frame checks, it uses a simple loop and processes only the necessary players.

I could use this but the only problem is that .Magnitude generates a spherical hitbox around the player. So the hitbox isnt really that accurate and doesnt match the overall “shape of the player”. Also im am pretty sure this is ping dependant because this is running on the server and not the client (at least thats what i think, i could be wrong tho).

1 Like

Sry for the inconvenience, i appreciate the help tho.

I could be wrong also XD but I want to say—server-side checks can feel ping-dependent because the server processes the last known positions of players, which might be slightly outdated if a player has high ping. However, the actual distance calculations themselves aren’t affected by ping—it’s just the data being used (player positions) that can cause discrepancies.

To make it feel more responsive, you can run the initial detection on the client and then validate the tag on the server. This way, the tagging feels instant on the client side, while the server ensures fairness.

I want to say The distance calculations (whether via .Magnitude or bounding box checks) are lightweight mathematical operations that the server is already performing as part of its normal physics and gameplay updates.

  • So No Extra Work: By performing calculations like .Magnitude or bounding box checks, you’re simply using data that the server already has in memory.
  • No Redundant Data: The server doesn’t fetch new data or perform additional network calls for these calculations—it uses the existing CFrame or Position properties of the players’ characters.

I really should of used bounding box instead of . magnitude, magnitude is slightly better performance wise if i remember correctly but its negligible if i remember correctly so XD
No worries on the inconvenience, i’d like to give this one more shot at least hopefully this other method fits your desired goals better.

---Client Script local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local TagEvent = ReplicatedStorage:WaitForChild("TagEvent") -- RemoteEvent for tagging local player = Players.LocalPlayer local character = player.Character or player.CharacterAdded:Wait() local humanoidRootPart = character:WaitForChild("HumanoidRootPart") local tagCheckFrequency = 0.1 -- Throttle checks to every 0.1 seconds local hitboxPadding = Vector3.new(1, 1, 1) -- Adjust hitbox size -- Function to check bounding box overlap local function isOverlapping(partA, partB, padding) local sizeA, posA = (partA.Size + padding) / 2, partA.Position local sizeB, posB = (partB.Size + padding) / 2, partB.Position return (math.abs(posA.X - posB.X) <= sizeA.X + sizeB.X) and (math.abs(posA.Y - posB.Y) <= sizeA.Y + sizeB.Y) and (math.abs(posA.Z - posB.Z) <= sizeA.Z + sizeB.Z) end -- Periodic tagging checks while true do for _, otherPlayer in ipairs(Players:GetPlayers()) do if otherPlayer ~= player and otherPlayer.Character then local otherHumanoidRootPart = otherPlayer.Character:FindFirstChild("HumanoidRootPart") if otherHumanoidRootPart and isOverlapping(humanoidRootPart, otherHumanoidRootPart, hitboxPadding) then -- Notify the server of a potential tag TagEvent:FireServer(otherPlayer) end end end task.wait(tagCheckFrequency) end 
---SeverScript local ReplicatedStorage = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") local TagEvent = Instance.new("RemoteEvent", ReplicatedStorage) TagEvent.Name = "TagEvent" local tagCooldown = 1 -- Cooldown in seconds local taggedPlayers = {} local hitboxPadding = Vector3.new(1, 1, 1) -- Ensure server validation matches client padding -- Function to check bounding box overlap local function isOverlapping(partA, partB, padding) local sizeA, posA = (partA.Size + padding) / 2, partA.Position local sizeB, posB = (partB.Size + padding) / 2, partB.Position return (math.abs(posA.X - posB.X) <= sizeA.X + sizeB.X) and (math.abs(posA.Y - posB.Y) <= sizeA.Y + sizeB.Y) and (math.abs(posA.Z - posB.Z) <= sizeA.Z + sizeB.Z) end -- Handle tag requests TagEvent.OnServerEvent:Connect(function(player, targetPlayer) if taggedPlayers[targetPlayer.UserId] then return end -- Check cooldown local character = player.Character local targetCharacter = targetPlayer.Character if character and targetCharacter then local rootPart = character:FindFirstChild("HumanoidRootPart") local targetRootPart = targetCharacter:FindFirstChild("HumanoidRootPart") if rootPart and targetRootPart and isOverlapping(rootPart, targetRootPart, hitboxPadding) then -- Perform tag logic print(player.Name .. " tagged " .. targetPlayer.Name) -- Add cooldown for the tagged player taggedPlayers[targetPlayer.UserId] = true task.delay(tagCooldown, function() taggedPlayers[targetPlayer.UserId] = nil end) end end end) 

Enhancements

1. Hitbox Padding

  • By adding Vector3.new(1, 1, 1) to the HumanoidRootPart size in the bounding box check, the hitbox becomes slightly larger to account for animation variations or ping-induced discrepancies.

2. Client-Side Throttling

  • The tagCheckFrequency limits the client-side loop to run every 0.1 seconds instead of every frame. This reduces the load on the client and the number of remote events fired.

3. Client-Server Hybrid Approach

  • The client handles the initial detection to make the game feel responsive, while the server validates the tag for accuracy and fairness.

4. Cooldown System

  • Prevents multiple tags from being registered in a short period, avoiding abuse or unintended behavior.

5. Server Validation

  • The server ensures that only valid tags are registered, protecting against client-side manipulation or false positives.

XD hope this does it, these new enhancements should all bet much better. I can explain why to the best of my ability if it helps.

1 Like

Hey @HolyGenki so sry for the late reply. I was kinda busy irl. So i did some research and found out that a more modern, updated technique were spatial queries. I wanted to use :GetPartBoundsInBox but the hitbox needs to be welded to character. Is there any way I can do that?

no worries that’s life. I was really hoping some one more experience than me in this field would’ve popped in to help by now as well. Like has a tagging sim or something completed and really got a good idea on how to do this better.

I haven’t used :getpartboundsInbox before so Ima need to study a little bit.

You should be able to just weld it on respawn then set the part to massless and non-collide (so it doesn’t affect physics).

You can use Player.PlayerAdded to get new players then Player.CharacterAdded to get newly spawned characters.

There might be some weird stuff with the welds replicating (sometimes they like to auto set themselves to different values on the server and client when they’re added to moving parts). If worst comes to worst you can use a rigid constraint with attachments, which should always replicate properly.

1 Like

That is some solid advice about welds, I’ve had this problem before, and total forgot it was a thing. XD
They defiantly didn’t handle high speeds well for me.

1 Like

Alright i think i Crammed the new information in my head the pro’s and cons to this GetPartBoundsINBox and i changed it to Rigid Constraints since the hit box will be moving around a lot more while still keeping the logic on making sure the tag was legit.

--Client local Players = game:GetService("Players") local function createHitbox(character) local humanoidRootPart = character:WaitForChild("HumanoidRootPart") -- Create the hitbox part local hitbox = Instance.new("Part") hitbox.Name = "Hitbox" hitbox.Size = Vector3.new(4, 6, 4) -- Adjust size as needed hitbox.Transparency = 1 hitbox.CanCollide = false hitbox.Massless = true hitbox.Anchored = false hitbox.Parent = character -- Create attachments local rootAttachment = Instance.new("Attachment") rootAttachment.Name = "RootAttachment" rootAttachment.Parent = humanoidRootPart local hitboxAttachment = Instance.new("Attachment") hitboxAttachment.Name = "HitboxAttachment" hitboxAttachment.Parent = hitbox -- Create the RigidConstraint local constraint = Instance.new("RigidConstraint") constraint.Attachment0 = rootAttachment constraint.Attachment1 = hitboxAttachment constraint.Parent = humanoidRootPart -- Can be parented to either part print("Hitbox created and attached with RigidConstraint for", character.Name) end -- Listen for character spawning Players.PlayerAdded:Connect(function(player) player.CharacterAdded:Connect(function(character) createHitbox(character) end) end) 
--Server Script local ReplicatedStorage = game:GetService("ReplicatedStorage") local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") -- Create RemoteEvent for tagging local TagEvent = Instance.new("RemoteEvent", ReplicatedStorage) TagEvent.Name = "TagEvent" local tagCooldown = 1 -- Cooldown in seconds local taggedPlayers = {} -- Handle tag requests (triggered by the client) TagEvent.OnServerEvent:Connect(function(player) local character = player.Character if not character then return end local hitbox = character:FindFirstChild("Hitbox") if not hitbox then return end -- Perform spatial query to find overlapping parts local overlappingParts = Workspace:GetPartBoundsInBox(hitbox.CFrame, hitbox.Size) for _, part in ipairs(overlappingParts) do local targetCharacter = part.Parent local targetPlayer = Players:GetPlayerFromCharacter(targetCharacter) -- Check valid target and ensure cooldown isn't active if targetPlayer and targetPlayer ~= player and not taggedPlayers[targetPlayer.UserId] then -- Process the tag print(player.Name .. " tagged " .. targetPlayer.Name) -- Add cooldown for the tagged player taggedPlayers[targetPlayer.UserId] = true task.delay(tagCooldown, function() taggedPlayers[targetPlayer.UserId] = nil end) end end end) 
1 Like