Creating a Battlegrounds-like Hitbox

INTRODUCTION

You might have played top battlegrounds games, most notably The Strongest Battlegrounds. As a programmer, you may be wondering how they created their hitboxes, especially if you are an inspiring developer planning on creating something similar. In this tutorial, I will guide you on how to create a professional punch hitbox. Keep in mind that this will not carry you. To create a worthy punching system, you need to address the following:

  1. Sound Effects. You need to have good sound effects to really feel the kick in your punches.
  2. Animations. Very important. Without good animations, your punches will feel stale.
  3. Visual Effects. This contributes to your overall game design. Your punches must not feel bland.

Keep in mind that I will not be addressing these key details because this is a tutorial for your hitboxes.

MAIN IDEA

First of all, we need to figure out the way we will be creating the hitbox. This is the bread and butter.
If you are a beginner developer, you may suggest Touched Events. Please for the love of everyone in the Devforum, do not use Touched Events. They take up more resources than necessary, and is more complicated than the solution, which I will get into later.

If you are an intermediate developer, you might suggest Raycasting. This is a more viable option than Touched Events and you may use this, but I wouldn’t recommend, since you need several Raycasts to cover a larger area.

Spatial Queries

However, there exists Spatial Queries. There are several of them, but most notably, there are 3 main options. The following all are built-in roblox functions used on workspace.

  1. GetPartsInPart() is more accurate but takes up more resources because you need to have a part in workspace

  2. GetPartsBoundInRadius() is reliable, but is only used for spherical hitboxes

  3. GetPartsBoundInBox() is also reliable, but instead is used for cubical hitboxes, and doesn’t take up many resources.

These options may vary depending on what hitbox you are going for, but I highly recommend GetPartsBoundInBox(). For this tutorial, I will be using GetPartsBoundInBox() because of its utility in our scenario.

SCRIPTING

Now we get to the sauce of making your hitbox. Many people will recommend using a ServerScript but this is super unreliable. Using this will make everything happen Server-sided, so if you have high ping, it will feel very unfair because it looks like you hit your victim, when you didn’t.

However, using a LocalScript instead of a ServerScript is like switching out airpods for headphones. Everything now happens on your client which as a result, you actually hit the victim when you are supposed to. Beginner programmers will often look over Remote Events, which you can use to send a signal for the player who got hit to the server, which will then do the extra damage, stun, etc.

Here’s how you put it into action:

local plr = game.Players.LocalPlayer
local char = plr.Character

local HBsize = Vector3.new(4.6, 4, 5.75) -- change to your hitbox size
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams.FilterDescendantsInstances = {char}

local parts = workspace:GetPartBoundsInBox(hrp.CFrame * CFrame.new(0, 0.25, -HBsize.Z/2), HBsize, overlapParams)
local plrsHit = {} -- table for all the players who were caught in the hitbox

for _, v in pairs(parts) do
	if v.Parent and v.Parent:FindFirstChild("Humanoid") and v.Parent:FindFirstChild("HumanoidRootPart") then

		local EChar = v.Parent

		if not table.find(plrsHit, EChar) then table.insert(plrsHit, EChar) end

	end
end

After, you then just send a signal through a RemoteEvent.
Now, here’s a step-by-step explanation on what the code does:

  1. First, we create our basic variables for the player who will create the hitbox.
  2. Then, we configure the settings our hitbox by listing the size of our hitbox, and by create parameters. FilterType = Enum.RaycastFilterType.Exclude will make everything in the FilterDescendantsInstances table be excluded for the hitbox, and FilterDescendantsInstances = {char} will make our character be excluded.
  3. Afterwards, we create another variable. It is what makes the hitbox. The first parameter is the CFrame of where our hitbox will be created, the second parameter is the size of our hitbox, then the third parameter is our Overlap Parameters, where we excluded us from being in the hitbox.

However, the variable will actually only return us a table full of parts which were caught in the hitbox. Instead, we would like to know the players caught in it. To do that, we create an empty table, then we execute a for loop for the parts variable, where we creating the hitbox. Remember, that variable is only full of parts that was in the hitbox.

Using the for loop, we check if the part has a parent, and if the parent has a Humanoid and a HumanoidRootPart. By doing that, we know if we detected a player, and from there we create a variable for the enemy character and insert it into the empty table that we had.

So, by doing that, we now have a table with the players caught in the hitbox!

EXTRA INFO

  • To get the amount of players caught, you would just to #plrsHit which will return a number of how many objects (or how many players) were in the table.
  • To actually damage the players or configure the players, you would create a for loop for the plrsHit table, where v is the character of the player. Then you would do something like
v.Humanoid:TakeDamage(number)

If you have any questions, do not hesitate to ask. Have a great day!

15 Likes

I actually am making a JoJo combat game right now and I have faced this exact problem…? How should i calculate my hitboxes, I like this Spatial Queries approach, It is indeed faster and better. But what do you think about MuchachosHitbox, or HitboxClass, or ZonePlus for hitboxes…?

For Example MuchachosHitbox has Velocity Predictions, that predict where the hitbox should be so it is way more accurate, sure you can do that yourself too but I feel like having the module do it for you is pretty neat.

Only downside as you mentioned, is that it’s using the Hitbox.Touched event and TouchEnded event.

You could make a hitbox module that uses Spatial Queries, and has velocity prediction. That feature is really good…

The Cyan Hitbox is using velocity prediction.
The Red Hitbox doesn’t use velocity prediction.

So which module should you use?

MuchachosHitbox because of its Velocity Predictions?

HitboxClass Because its fast and its OOP?

Or ZonePlus Because it uses Spatial Queries?

In Strongest battleground mentioned above, the Hitbox system is done entirely through the server and does not use RemoteEvents to transfer information from client to server about who hit who.

I have a module that exactly replicates the Hitbox system in Strongest battleground, using 2 modules.

HitboxCaster - which via Bindable event passes data to the script when someone was hit and tracks :GetTouchingParts().

HitboxModule - which creates a hitbox and binds it to a player via Weld.

I think your idea to transmit hits through the client is very bad, as there are many nuances besides accuracy.
Exploiters can take advantage of this, and even if there is a defense there is always a workaround.
If your opponent is pinging too, for you he can hit the air and hit you, even though on your screen you will be far away from him.
Also RemoteEvents has a delay in transferring information from client to server, so the hits will be out of sync, and you need to replicate through the client if you do it your way

So I think that server Hitbox Modules are the most reliable and the best.
Also lags are prevented by manually clearing various RBXScriptConnection and other data that accumulates in the server memory.

Modules like Trove, Maid and others.

Also in Strongest Battleground is used module (which I will attach). This module makes synchronization between players to have a smooth gameplay.

local Thread = {}

local RunService = game:GetService("RunService")

local threads = {}

RunService.Stepped:Connect(function ()
	local now = tick()
	local resumePool

	for thread, resumeTime in pairs(threads) do
		-- Resume if we're reasonably close enough.
		local diff = (resumeTime - now)

		if diff < 0.005 then
			if not resumePool then
				resumePool = {}
			end

			table.insert(resumePool, thread)
		end
	end

	if resumePool then
		for _,thread in pairs(resumePool) do
			threads[thread] = nil
			coroutine.resume(thread, now)
		end
	end
end)

function Thread:Wait(t)
	if t ~= nil then
		local t = tonumber(t) or 1 / 30
		local start = tick()

		local thread = coroutine.running()
		threads[thread] = start + t

		-- Wait for the thread to resume.
		local now = coroutine.yield()
		return now - start, os.clock()
	else
		RunService.Heartbeat:Wait()
	end
end

function Thread:Spawn(callback)
	task.spawn(callback)
end

function Thread:Delay(t, callback)
	task.delay(t, callback)
end

return Thread