Advanced server hit detection

i’ve previously asked questions on moving projectiles and how to do hit detection on the server if the projectile is visible only to the client, which was solved with raycasting

HOWEVER, raycasting doesn’t seem to fix all solutions. For example, if i wanted a projectile to leave a lingering field/area that could hurt others, how would i do the hit detection there if the server doesn’t have the projectile/field? i don’t think raycasts would be helpful in such a solution since you’d have to fire many at once over and over (especially if the field is large) and that could generate a lot of lag, and i am not sure if region3 would be helpful

what should be done for hit detection in this case?

4 Likes

You can use RemoteEvents and :FireAllClients(), and then the clients will make a client-sided field.

theres a few different options you have on your displosal
Option1:
You create a part/multiple parts, and fire for everything it touches (of course on an interval)
Option2:
Create a sort of “dynamic” region3/magnitude region and go off from that (i don’t know how intensive that would be on the server, but i personally haven’t had problems with that yet)

is it really a good idea to trust hit detection on the client though?
how would i validate these hit detections on the server?

I’m still not 100% certain about raycasting on client but so far it’s the best way to perform raycasting for guns in my opinion. In my game, I used the client to do ray casting. It will also Invoke the server for taking damage and bullet visuals etc. However, it will decline the shot if the server receive time is larger than a certain amount of time. Currently it’s 2 seconds for me, that will decline a shot when player has around of 1.5-2k ping. I’m deciding to lower that to 1 - 1.5 second, which is around 800 ping.

I think it’s somewhat impossible for exploiters to access client side read only code to do wacky actions/modifications to the raycasting code, unless you’re using a client module script where exploiters can easily require it. Correct me if I’m wrong, I’m not sure.

1 Like

so would it be fine to just do the hitdetection on the client?
i’m just worried that an exploiter could delete the localscript(s) and would gain an obvious advantage or use the event to deal damage arbitrarily (your suggestion could work, even though it’s a different genre) but maybe i might be simply worrying too much on the subject of exploiters

it’s obvious i need to put sanity checks, but i’m not sure how exactly especially when there’s slow moving bullets and all that

Currently I do hit detection on client but I also do a variety of checks on both client and server. Your concern does not apply to my case, since I use one big client local script to cover everything, from UI buttons to weapon raycasting, it’s all in one, if an exploiter disables the script, everything will not work for him. Again, correct me if I’m wrong! I’m not 100% certain about this.

For dealing damage, you should always do that on server, not only on the humanoid:TakeDamage, also on the way that how the damage is calculated. If a client fires something like TakeDamage:FireServer(99999), it’s most likely it’s an exploiter and you should decline that on server. Just do checks as much as you can!

2 Likes

I’d argue that’s a bad way to do this, I’m aware this is a necropost, but I have a much better way to do this.

So, I do the raycasting on the client using @EgoMoose’s UserInputService Mouse module. (I’m a big fan of open source modules) I then fire a RemoteEvent which then fires all of the clients to render the bullets. There’s a tick for when the bullet is shot, and there’s a tick for when the bullet is finished. The bullet is also given a unique id that I use to check which tween was finished. I have the clients have a Touched event where it fires an ended event with the “Unique Id” I mentioned earlier. So here’s where the hit registration comes in. I use @EgoMoose’s RotatedRegion3 to do hit registration. I take the beginning tick and the ended shot tick and subtract them which gives how long it’s been since the tween ended. I take this time and divide it by the time it took to tween the bullets (Credit to @Headstackk’s thread for the equation) This gives me a value from 0 to 1 that I can use to lerp. I can lerp this to get the rough position of where the bullet hit. I used the RotatedRegion3 with the rough position and size of the bullet and I get the touching parts of the RotatedRegion3. Finally, I iterate through all of the parts and look for which one has a humanoid and I check if the players are also on the same team. Then, if it finds the humanoid, it takes damage. Here’s the code:

-- server
local HttpService = game:GetService("HttpService")
local Debris = game:GetService("Debris")
local TweenService = game:GetService("TweenService")
local Players = game:GetService("Players")

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local TimeSyncService = require("TimeSyncService")
local RotatedRegion3 = require("RotatedRegion3")
TimeSyncService:Init()

local GetRemoteEvent, GetRemoteFunction = require("GetRemoteEvent"), require("GetRemoteFunction")
local newClock = TimeSyncService:WaitForSyncedClock()

local RENDER_BULLET_EVENT = GetRemoteEvent("RenderBullet")
local BULLET_SHOOT_FINISHED = GetRemoteEvent("BulletFinished")

local IKService = require("IKService")
IKService:Init()

local BULLET_VELOCITY = 50

local projectileTable = {}

local BULLET_SIZE = Vector3.new(0.8, 0.2, 0.2)

function IsTeamMate(Player1, Player2)
	return (Player1 and Player2 and not Player1.Neutral and not Player2.Neutral and Player1.TeamColor == Player2.TeamColor)
end

RENDER_BULLET_EVENT.OnServerEvent:Connect(function(player, startCFrame, endCFrame, damage, mainPart)
	local projectileId = HttpService:GenerateGUID(false)
	projectileTable[projectileId] = newClock:GetTime()
	
	endCFrame = CFrame.fromMatrix(endCFrame.Position, endCFrame.LookVector, endCFrame.UpVector, endCFrame.RightVector)
	startCFrame = CFrame.fromMatrix(startCFrame.Position, endCFrame.RightVector, endCFrame.UpVector, -endCFrame.LookVector)
	
	local time = (endCFrame.Position - startCFrame.Position).Magnitude * 0.3 / BULLET_VELOCITY
	RENDER_BULLET_EVENT:FireAllClients(player, time, startCFrame, endCFrame, projectileId, mainPart, projectileTable[projectileId])
	
	local connection
	connection = BULLET_SHOOT_FINISHED.OnServerEvent:Connect(function(_, newProjectileId)
		if projectileId == newProjectileId then
			local endTime = newClock:GetTime()
			local difference = endTime - projectileTable[projectileId]
			
			local alpha = TweenService:GetValue(difference / time, Enum.EasingStyle.Linear, Enum.EasingDirection.In)
			local roughPosition = startCFrame:Lerp(endCFrame, alpha)
			
			local newRegion3 = RotatedRegion3.new(roughPosition, BULLET_SIZE)
			local parts = newRegion3:FindPartsInRegion3(player.Character)
			
			local humanoid 
			for _, part in ipairs(parts) do
				humanoid = part.Parent:FindFirstChildWhichIsA("Humanoid") or part.Parent.Parent:FindFirstChildWhichIsA("Humanoid")
				if humanoid then
					break
				end
			end
			
			if humanoid then
				local attackPlayer = Players:GetPlayerFromCharacter(humanoid.Parent)
				if attackPlayer and IsTeamMate(player, attackPlayer) then
					return
				end
				
				local Creator_Tag = Instance.new("ObjectValue")
				Creator_Tag.Name = "creator"
				Creator_Tag.Value = attackPlayer
				Debris:AddItem(Creator_Tag, 2)
				Creator_Tag.Parent = humanoid
				humanoid:TakeDamage(damage)
			end
			
			projectileTable[projectileId] = nil
			connection:Disconnect()
		end
	end)
end)
-- client
local player = game.Players.LocalPlayer

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")

local require = require(game:GetService("ReplicatedStorage"):WaitForChild("Nevermore"))

local PartCache = require("PartCache")
local TimeSyncService = require("TimeSyncService")
local GetRemoteEvent, GetRemoteFunction = require("GetRemoteEvent"), require("GetRemoteFunction")

local camera = workspace.CurrentCamera

TimeSyncService:Init()

local newClock = TimeSyncService:WaitForSyncedClock()

local RenderBullet = GetRemoteEvent("RenderBullet")
local BulletFinished = GetRemoteEvent("BulletFinished")

local bullet = game:GetService("ReplicatedStorage"):WaitForChild("Bullet")
local cache = PartCache.new(bullet, 25)
cache:SetCacheParent(workspace:WaitForChild("CachedParts"))

local Spring = require("QuentySpring")
local cameraSpring = Spring.new(Vector3.new())
cameraSpring.Damper = 0.5

RenderBullet.OnClientEvent:Connect(function(player, tweenTime, startCFrame, endCFrame, projectileId, mainPart, time0)
	local newBullet = cache:GetPart()
	newBullet.Parent = workspace.GameStuff
	newBullet.CFrame = startCFrame
	
	local tweenConnection, alpha
	
	local function ended()
		BulletFinished:FireServer(projectileId)
		tweenConnection:Disconnect()
		
		newBullet.Trail.Enabled = false
		
		if not newBullet:IsDescendantOf(workspace.CachedParts) then
			cache:ReturnPart(newBullet)
		end
	end
	
    tweenConnection = RunService.RenderStepped:Connect(function(delta)
        local time = newClock:GetTime() + delta
        local currentTime = time - time0
        alpha = currentTime / tweenTime
        alpha = TweenService:GetValue(alpha, Enum.EasingStyle.Linear, Enum.EasingDirection.In)
		local position = startCFrame:Lerp(endCFrame, alpha)
        newBullet.CFrame = position -- lerp the bullet’s cframe so it’s synced with all other clients
		
		if alpha >= 1 then
			ended()
		end
    end)

	newBullet.Trail.Enabled = true
	
	if mainPart then
		for _, particleEmitter in ipairs(mainPart:GetChildren()) do
			if particleEmitter:IsA("ParticleEmitter") then
				particleEmitter:Emit(10)
			end
		end
	end
	
	local connection
	connection = newBullet.Touched:Connect(function(hit)
		if not hit:IsDescendantOf(player.Character) then
			ended()
		end
	end)
	
	cameraSpring:Impulse(Vector3.new(0, 1, 0.5) / (player:DistanceFromCharacter(newBullet.Position) + 1))
end)

local RunService = game:GetService("RunService")
RunService.RenderStepped:Connect(function(delta)
	cameraSpring:TimeSkip(delta * 16)
	local cameraCFrame = cameraSpring.Position
	camera.CFrame = camera.CFrame * CFrame.Angles(cameraCFrame.Z / 10, 0, cameraCFrame.Y / 5)
end)

Remember, never have the server depend on data that can be easily manipulated, especially if it can cause a player to have an advantage.

(I also use @EtiTheSpirit’s PartCache and modules from @Quenty’s NevermoreEngine).

8 Likes