Is this method of validating a "hit" valid? Serverside v clientside and hitboxes

Hello, I scripted a punching system that I would appreciate feedback for(is it bad, good, could use work, etc).

It involves GetPartsBoundinBox, and most of the work is done by the client e.g. creating a hitbox, determining the parts in the hit box, and detecting a humanoid “hit”(I’ll explain right underneath).

The server’s job is to validate if the “hit” is valid. What it does is it compares the properties of the AttackFunction from the serverside module script and the clientside script(both of which are pretty much contain the same information) to see if there were changes on the clientside(e.g. hitbox size change) to prevent exploiting. It then detects if the client’s Player’s HumanoidRootPart’s position of the given and the server/workspace Player’s HumanoidRootPart’s position differ to account for latency. It also compares the player’s position and the humanoid “hit” position and see if it is more than a certain value to prevent exploiting

Overall, I would like to know if my code could cause lag, be inefficient(is there a better way?), etc

Here is the client side code(it’s in StarterCharacterScripts)

--// Services //--

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UIS = game:GetService("UserInputService")

--// Variables //--

local AttackInformation = require(ReplicatedStorage:WaitForChild("AttackInformationClient"))
local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local HRP = Character:WaitForChild("HumanoidRootPart")
local PunchEvent = ReplicatedStorage:WaitForChild("PunchEvent")
local MoveEquipped = false

--// Main //--

UIS.InputBegan:Connect(function(input, gameProcessed)
	if Character.Humanoid:GetState() ~= Enum.HumanoidStateType.Dead then
		if input.UserInputType == Enum.UserInputType.MouseButton1 and not gameProcessed and MoveEquipped == false then  
			local HitDetected = AttackInformation.AttackInitiated(AttackInformation.PunchAttack,HRP)
			if HitDetected then
				PunchEvent:FireServer(AttackInformation.PunchAttack, HRP, HitDetected)
			end
		end
	end
end)

Serverside hit confirmation

--// Services //--

local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

--// Variables //--

local AttackInformation = require(ServerScriptService.AttackInformationServer)
local PunchEvent = ReplicatedStorage:WaitForChild("PunchEvent")
local HitFail = false
local MaxLatencyDistanceHRP = 1 --maximum distance between client HRP and server HRP allowed due to latency

--// Main //--

PunchEvent.OnServerEvent:Connect(function(player, AttackUsed, ClientHRP, Target)
	local ServerHRP = workspace:FindFirstChild(ClientHRP.Parent.Name):FindFirstChild("HumanoidRootPart")
	local UserHRPDistances = (ClientHRP.Position - ServerHRP.Position).Magnitude
	local MaxLatencyDistanceHitbox = AttackInformation.PunchAttack.MaxLatencyDistanceHitbox --changes between Attacks
	local UserHRPTargetDistance = math.abs(UserHRPDistances - AttackInformation.PunchAttack.HitboxSize.Z) --changes between attacks
	for i, v in pairs(AttackUsed) do
		if v ~= AttackInformation.PunchAttack[i] then --changes between attacks
			warn("AttackUsedValue differs. Possible exploiter")
			HitFail = true
			break
		end
	end
	if not HitFail then
		if UserHRPDistances < MaxLatencyDistanceHRP and UserHRPTargetDistance < MaxLatencyDistanceHitbox then
			Target.Parent.Humanoid.Health = Target.Parent.Humanoid.Health - 10
		end
	else
		HitFail = false
	end


end)

clientside module script with attack properties

local Attack = {

	PunchAttack = {

		HitboxSize = Vector3.new(4, 4.998, 3.364),
		HitboxOffset = Vector3.new(0,0,-2),
		HitboxCanCollide = false,
		HitboxAnchored = false,
		HitboxTransparency = 1,
		Cooldown = 1,
		HitboxDespawn = 0.1,
		HitboxParent = workspace

	}


}

local AttackTemplate = {

	HitboxSize = Vector3.new(4, 4.998, 3.364),
	HitboxOffset = Vector3.new(0,0,-2),
	HitboxCanCollide = false,
	HitboxAnchored = false,
	HitboxTransparency = 1,
	Cooldown = 1,
	HitboxDespawn = 0.1,
	HitboxParent = workspace -- just to visualize the hitbox

}

function Attack.AttackInitiated(AttackName,HRP)
	local debounce = false
	local HitDetected = nil
	local Target
	local Attack = AttackName
	local Hitbox = Instance.new("Part")
	local HitboxDespawn = Attack.HitboxDespawn
	local HitboxOffset = CFrame.new(Attack.HitboxOffset)
	local Cooldown = Attack.Cooldown
	local HitboxParams = OverlapParams.new()
	HitboxParams.FilterDescendantsInstances = {HRP.Parent:GetChildren(), Hitbox}
	HitboxParams.FilterType = Enum.RaycastFilterType.Exclude
	Hitbox.Size = Attack.HitboxSize
	Hitbox.CanCollide = Attack.HitboxCanCollide
	Hitbox.Anchored = Attack.HitboxAnchored                                                
	Hitbox.Transparency = 0.5 													--make sure to change
	local Weld = Instance.new("Weld")
	Weld.Part0 = HRP
	Weld.Part1 = Hitbox
	Weld.C1 = CFrame.new(0,0,2)														--change
	Weld.Parent = HRP
	Hitbox.Parent = Attack.HitboxParent

	if Hitbox then
		while true do
			local HitboxObjects = workspace:GetPartBoundsInBox(Hitbox.CFrame, Hitbox.Size, HitboxParams)
			if #HitboxObjects ~= 0 then
				for i, v in pairs (HitboxObjects) do
					if v.Parent:FindFirstChild("Humanoid") and v.Parent:FindFirstChild("Humanoid").Health ~= 0 and debounce == false then
						debounce = true
						task.wait(HitboxDespawn)
						Hitbox:Destroy()
						debounce = false
						HitDetected = true
						Target = v.Parent:FindFirstChild("HumanoidRootPart")
						break
					else
						task.wait(HitboxDespawn)
						Hitbox:Destroy()
						break
					end
				end
			else
				task.wait(HitboxDespawn)
				Hitbox:Destroy()
			end
			task.wait(HitboxDespawn)
			break
		end
	end
	if HitDetected then
		HitDetected = false
		return Target
	end

end

return Attack

serverside module script with attack properties

local Attack = {

	PunchAttack = {

		HitboxSize = Vector3.new(4, 4.998, 3.364),
		HitboxOffset = Vector3.new(0,0,-2),
		HitboxCanCollide = false,
		HitboxAnchored = false,
		HitboxTransparency = 1,
		Cooldown = 1,
		HitboxDespawn = 0.1,
		MaxLatencyDistanceHitbox = 5,
		HitboxParent = workspace

	}


}

local AttackTemplate = {

	HitboxSize = Vector3.new(4, 4.998, 3.364),
	HitboxOffset = Vector3.new(0,0,-2),
	HitboxCanCollide = false,
	HitboxAnchored = false,
	HitboxTransparency = 1,
	Cooldown = 1,
	HitboxDespawn = 0.1,
	MaxLatencyDistanceHitbox = 5,
	HitboxParent = workspace -- just to visualize the hitbox

}

return Attack

2 Likes

Hello, can you please tell me what that line means

1 Like

Hello, I’d be happy to help. UserHRPDistances (The distance between the user’s humanoid root part on the client and the user’s humanoid root part on the server; this is to account for any latency) is subtracted from the attack’s (in this case it is a "punch attack) hitbox size in the z direction. This gives us UserHRPTargetDistance which is compared with MaxLatencyDistanceHitbox. To be honest, it isn’t super necessary since its purpose is to check if a client’s hitbox differs by a certain amount then the server’s hitbox

I forgot, but the question was answered here: Is this way of doing hitboxes efficient/effective?

I also posted an update version here: Simple Hitbox for melee attacks using GetPartsBoundInBox

Not the best, but gets the job done

It still doesn’t make sense for me. What does it check and what is its purpose?

TLDR; The client and server hitboxes differ because of latency so I compared the two hitboxes to see if they exceed a certain amount of units. This is to prevent exploiters from extending their hitbox into the far distance and it also prevents super laggy players from hitting you across the map even though you don’t see them on your screen. I hope that makes sense. If it doesn’t, I’ll try again lol

The client and serverside hitboxes differ a bit because of latency. This is more noticeable when moving.

For example, if I’m moving and a spawn in a hitbox, on the client it appears normal, but the server sees that the hitbox “lags” behind a bit.

You can test this phenomenon by having a local script and a server script both with code that basically spawns in front of the player when pressing a key. When moving, on the local script, it’ll appear in front as normal; however, on the server script, the box appears to “lag” behind.

Back to the code. Now that we understand latency, I used a local script in order to not make the hitbox “laggy” but then verified it using the server. That’s where the code you mentioned comes in. If I notice that the difference between what the server sees and what the client sees exceeds a certain amount of units, then I know that the player is either exploiting by extending the hitbox range or is super laggy.

Note that UserHRPTargetDistance is compared to MaxLatencyDistanceHitbox(this is set to 5). You can change MaxLatencyDistanceHitbox to as high as you want. The higher it is, the more “grace” you give to the player if they are lagging; however, setting it to 0 means that there is no “grace,” so if the hitboxes on the server and client differ by a minimal amount of units, the code will not run.

here’s a picture: UserHRPDistances is the distance between what the client sees and what the server sees. If it is greater than MaxLatencyDistanceHitbox then the “hit” is not valid

I know what latency is. I was talking about your formula of TargetDistance. Why do you substract Hitbox size from the latency in positions ?

I have a question, why do you bother trying to calculate the latency? In my opinion, I wouldn’t do this unless your game involves something specific. Normally, there shouldn’t be a noticeable difference, and by estimating the latency, there’ll non-deterministic results. However, please provide your reasoning.

Aside from that, there’s many improvements to be done regarding readability and general practice. There are inconsistencies in variable names on the client and server, magic numbers, and scope duplication. However I’d recommend for you to implement error handling the most.

Also, you could use: humanoid:TakeDamage(PUNCH_DAMAGE) instead of using negation.

Why not to calculate it? Even with normal Walkspeed of 16 latency in positions can reach 2 studs in roblox studio. It’s pretty bad for high pace combat games

As you said, you’re correct that latency’s bad for high-paced games. However from what I’m aware OP doesn’t seem to be developing that. Also, I’m unsure where you got that statistic from — it seems unlikely. Feel free to share but from my personal experience, I haven’t had latency problems. The implementation matters, I’ve developed multiple combat systems with combos, blocking, parries and more. I’m speaking personally though, I’m not trying to be arrogant and say you’re incorrect because as I said it’s my personal experience.

I was facing that problem in the past when I created Server sided hitbox. So I was testing that and found out that my latency was 1-2 studs when walking and 7-8 studs when dashing. That’s I just decided to create client sided hitbox with server validation like OP.

I see. I’m not going to doubt you and call you a liar but I haven’t had that experience. Regardless, you’re correct. I’m trying to say that there’s potential non-deterministic results. From my knowledge, Rocket League does something similarly. For example: A request will be sent when the car flips, the server will validate it and the client will be informed. The problem is that unless you explicitly need this validation you’re potentially causing more latency because there’s the request and the time for the request to be validated. I think this is good practice, however as I said unless you explicitly need it and are encountering those issues, you’re potentially causing more latency. As well as I said before inconsistent results, that request may be valid on the client, but invalid on the server therefor resulting in poor feedback.

I do that to compare the distances from the edge of the hitbox to the character’s humanoid root part. If the number is greater than say 5 then I know the hitbox is too far away from the character

So the variable that Kruizbeng mentioned is not really necessary. However, I think it might be able to prevent exploiters from extending their hitbox and also prevent super laggy players from hitting you from across the map even though the server sees them somewhere else. Basically, I’m making sure the client’s hitbox information and the server’s hitbox information don’t differ by a lot

Also, you’re right, this code is very messy, but this was my first attempt at this type of thing lol

I changed it to humanoid:TakeDamage() in my “updated” version although the same readability issues persist

Yeah, that’s also part of the issue. I guess it depends on what you are aiming for. Having all of your combat code and verification on the client side can lead to exploiters but provides for immediate feedback while the method I’m using, client side feedback and server side verification, is less likely to have exploiters because the hits are being verified by the server although it can lead to latency issues when it comes to a player taking damage

There shouldn’t be any possibility of exploiters extending their hitbox. If you’re doing the distance check via the server then there shouldn’t be any way to influence it, at least not directly.