How to Detect a Client-Sided Projectile from the Server?

Hi! I’ve come across a problem where I have Client-Sided Projectiles handling on all Clients, and when they hit a surface it fires a remote event on the Server that makes multiple hit registers where it should only happen once. This occurs because for every player (or client) loaded into the game, one person fires a projectile and it replicates that projectile to that person and all the other players in that server, which creates multiple projectiles on each client.

Sorry if I worded that out awkwardly so I will provide a video of it:
https://streamable.com/y7g2dj

As you can see when Player1 shoots a projectile it will replicate towards Player2’s client as I used a remote event from the Server to FireAllClients to let the clients know that Player1 fired a projectile. The issue is that when it hits the wall, the server thinks that 2 projectiles hit it and printed out x2 Hit texts in the output. This problem would be worse if there were 3 or more players in the server which in turn would have the server read multiple hit detections depending on how many players there are.

I’m using the FastCast Redux Module to handle the Projectile calculations on the client so that projectiles run smoothly:

local UserInputService = game:GetService("UserInputService")
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")

local FireButton = PlayerGui.ProjectileTest.TextButton

local ProjectilesFolder = workspace:FindFirstChild("ProjectilesFolder")
local ProjectileStorage = ReplicatedStorage:FindFirstChild("ProjectileStorage")

local ClientProjectiles = ReplicatedStorage:FindFirstChild("ClientProjectiles")
local FireProjectile = ReplicatedStorage:FindFirstChild("FireProjectile")
local ProjectileHit = ReplicatedStorage:FindFirstChild("ProjectileHit")

local FastCast = require(ReplicatedStorage:FindFirstChild("FastCastRedux"))
local castParams = RaycastParams.new()
castParams.FilterType = Enum.RaycastFilterType.Exclude
castParams.IgnoreWater = true

FastCast.VisualizeCasts = false

UserInputService.InputBegan:Connect(function (input, isTyping)
	if isTyping then return end
	
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		FireProjectile:FireServer(Character, HumanoidRootPart)
	end
end)

FireButton.MouseButton1Down:Connect(function()
	FireProjectile:FireServer(Character, HumanoidRootPart)
end)


local function onLengthChanged(cast, lastPoint, direction, length, velocity, projectile)
	if projectile then
		local projectileLength = projectile.Size.Z/2
		local offset = CFrame.new(0, 0, -(length - projectileLength))
		projectile.CFrame = CFrame.lookAt(lastPoint, lastPoint + direction):ToWorldSpace(offset)
		projectile.CFrame = projectile.CFrame:ToWorldSpace(CFrame.Angles(math.rad(-90), 0, 0))	
	end
end

local function CastTerminating(cast)
	local projectile = cast.RayInfo.CosmeticBulletObject
	Debris:AddItem(projectile, 0.01)
end

local function onRayHit(cast, result, velocity, projectile)
	local hit = result.Instance
	local hitPos = projectile.CFrame
	ProjectileHit:FireServer(hitPos, hit)
end

ClientProjectiles.OnClientEvent:Connect(function(Character, HumanoidRootPart)
	RunService.Stepped:Wait()
	--print(Index)
	local Missile = ProjectileStorage.Missile:Clone()

	local castBehavior = FastCast.newBehavior()
	castBehavior.RaycastParams = castParams
	castBehavior.AutoIgnoreContainer = false
	castBehavior.CosmeticBulletContainer = ProjectilesFolder
	castBehavior.CosmeticBulletTemplate = Missile
	castBehavior.MaxDistance = 250


	castParams.FilterDescendantsInstances = {Character, ProjectilesFolder}
	local caster = FastCast.new()

	local origin = HumanoidRootPart.Position
	local direction = (HumanoidRootPart.CFrame.LookVector).Unit

	local missileCast = caster:Fire(origin, direction, 30, castBehavior)
	
	missileCast.RayInfo.CosmeticBulletObject.CFrame = missileCast.RayInfo.CosmeticBulletObject.CFrame:ToWorldSpace(CFrame.Angles(math.rad(-90), 0, 0))
	
	caster.CastTerminating:Connect(CastTerminating)
	caster.LengthChanged:Connect(onLengthChanged)
	caster.RayHit:Connect(onRayHit)
end)

Then I have the FireAllClients function and the Server-Sided Hit detection handled on the server:

local HttpService = game:GetService("HttpService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")

local FireProjectile = ReplicatedStorage:WaitForChild("FireProjectile")
local ProjectileHit = ReplicatedStorage:WaitForChild("ProjectileHit")
local ClientProjectiles = ReplicatedStorage:WaitForChild("ClientProjectiles")

FireProjectile.OnServerEvent:Connect(function(plr, Character, HumanoidRootPart)
	ClientProjectiles:FireAllClients(Character, HumanoidRootPart)
end)

ProjectileHit.OnServerEvent:Connect(function(plr, HitPos, TargetHit)
	print("Hit")
	local Explosion = Instance.new("Explosion")
	Explosion.BlastPressure = 0
	Explosion.DestroyJointRadiusPercent = 0

	Explosion.Parent = workspace
	Explosion.Position = HitPos.Position

	Debris:AddItem(Explosion, 1)
end)

Is there a way where I can call a specific Client-Sided projectile to the Server so that it can only Hit once? All help is appreciated thanks!

1 Like

Alright, so basically there’s a thing called UserData where you can put whatever data you want into the cast, so I’ve made it so that the server would only do server stuff if the UserId from missleCast.UserData matches the UserId of the player that fired the event, and made a new RemoteEvent called “RenderHit” where it makes an explosion on the position where the cast ended up.

Client:

local UserInputService = game:GetService("UserInputService")
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local Players = game:GetService("Players")

local Player = Players.LocalPlayer
local PlayerGui = Player.PlayerGui
local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")

local FireButton = PlayerGui.ProjectileTest.TextButton

local ClientProjectiles = ReplicatedStorage:FindFirstChild("ClientProjectiles")
local FireProjectile = ReplicatedStorage:FindFirstChild("FireProjectile")
local ProjectileHit = ReplicatedStorage:FindFirstChild("ProjectileHit")
local RenderHit = ReplicatedStorage:FindFirstChild("RenderHit")

local ProjectilesFolder = workspace:FindFirstChild("ProjectilesFolder")
local ProjectileStorage = ReplicatedStorage:FindFirstChild("ProjectileStorage")

local FastCast = require(ReplicatedStorage:FindFirstChild("FastCastRedux"))
local castParams = RaycastParams.new()
castParams.FilterType = Enum.RaycastFilterType.Exclude
castParams.IgnoreWater = true

FastCast.VisualizeCasts = false

UserInputService.InputBegan:Connect(function (input, isTyping)
	if isTyping then return end

	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		FireProjectile:FireServer(Character, HumanoidRootPart, Player.UserId)
	end
end)

FireButton.MouseButton1Down:Connect(function()
	FireProjectile:FireServer(Character, HumanoidRootPart, Player.UserId)
end)

local function onLengthChanged(cast, lastPoint, direction, length, velocity, projectile)
	if projectile then
		local projectileLength = projectile.Size.Z/2
		local offset = CFrame.new(0, 0, -(length - projectileLength))
		projectile.CFrame = CFrame.lookAt(lastPoint, lastPoint + direction):ToWorldSpace(offset)
		projectile.CFrame = projectile.CFrame:ToWorldSpace(CFrame.Angles(math.rad(-90), 0, 0))	
	end
end

local function CastTerminating(cast)
	local projectile = cast.RayInfo.CosmeticBulletObject
	Debris:AddItem(projectile, 0.01)
end

local function onRayHit(cast, result, velocity, projectile)
	local hit = result.Instance
	local hitPos = projectile.CFrame
	ProjectileHit:FireServer(hitPos, hit, cast.UserData)
end

ClientProjectiles.OnClientEvent:Connect(function(Character, HumanoidRootPart, UserId)
	RunService.Stepped:Wait()
	--print(Index)
	
	local Missle = ProjectileStorage.Missle:Clone()
	
	local castBehavior = FastCast.newBehavior()
	castBehavior.RaycastParams = castParams
	castBehavior.AutoIgnoreContainer = false
	castBehavior.MaxDistance = 250
	castBehavior.CosmeticBulletContainer = ProjectilesFolder
	castBehavior.CosmeticBulletTemplate = Missle

	castParams.FilterDescendantsInstances = {Character}
	local caster = FastCast.new()

	local origin = HumanoidRootPart.Position
	local direction = (HumanoidRootPart.CFrame.LookVector).Unit
	
	local missileCast = caster:Fire(origin, direction, 30, castBehavior) 
	
	missileCast.UserData.UserId = UserId
	
	missileCast.RayInfo.CosmeticBulletObject.CFrame = missileCast.RayInfo.CosmeticBulletObject.CFrame:ToWorldSpace(CFrame.Angles(math.rad(-90), 0, 0))

	caster.CastTerminating:Connect(CastTerminating)
	caster.LengthChanged:Connect(onLengthChanged)
	caster.RayHit:Connect(onRayHit)
end)

RenderHit.OnClientEvent:Connect(function(HitPos)
	local Explosion = Instance.new("Explosion")
	Explosion.BlastPressure = 0
	Explosion.DestroyJointRadiusPercent = 0

	Explosion.Parent = workspace
	Explosion.Position = HitPos.Position

	Debris:AddItem(Explosion, 1)
end)

Server:

local HttpService = game:GetService("HttpService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")

local FireProjectile = ReplicatedStorage:WaitForChild("FireProjectile")
local ProjectileHit = ReplicatedStorage:WaitForChild("ProjectileHit")
local ClientProjectiles = ReplicatedStorage:WaitForChild("ClientProjectiles")
local RenderHit = ReplicatedStorage:WaitForChild("RenderHit")

FireProjectile.OnServerEvent:Connect(function(plr, Character, HumanoidRootPart, UserId)
	ClientProjectiles:FireAllClients(Character, HumanoidRootPart, UserId)
end)

ProjectileHit.OnServerEvent:Connect(function(plr, HitPos, TargetHit, UserData)
	if UserData.UserId == plr.UserId then
		--Do shenanigans like taking damage from the target
		
		RenderHit:FireAllClients(HitPos)
	end
end)
1 Like

Hi! I tried this and it seemed to work perfectly fine, but I found out that the hit detection is not always accurate because of ping differences. Do you know how to compensate for ping or lag for all clients in the server?

Never mind it’s not hit detection lag, but the projectile movement is not synced with all of the clients. Do I have to create a delay before firing a projectile on all clients?

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.