Why is my local projectile replicating to all players?

Hello! I’ve been working on a “magic projectile” over the last week and I had finally finished up until I tested it on a multiplayer server. Somehow, my client-made projectile is being replicated to all players. I.e., while having 3 players in a server, one playing activating their “magic attack” creates 3 projectiles viewable for every player.

My code is set up as:
Client calls remote event from the local script in their tool > server receives remote event via server script in the tool > server script fires the event back to all clients > local script in PlayerStarterScripts receives the event and the projectile is created there.

I have attempted to change the part’s parent location and the location from which it’s cloned but neither has helped.

Here is the code working properly with 1 player:
https://gyazo.com/eecacff4e41e9cd55aa446d5bda24c86

Here is the code not working properly with 2 players:
https://gyazo.com/6637ebe6f09a7aa3107d79b8dbdf31de

This is the local script that fires when the player activates the tool:

Summary
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteEvents = ReplicatedStorage:WaitForChild("RemoteEvents")
local FireballEvent = RemoteEvents:WaitForChild("FireballEvent")

local player = game:GetService("Players").LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local HumanoidRootPart = character:WaitForChild("HumanoidRootPart")

local holdingClick = false
local mouse = player:GetMouse()
mouse.TargetFilter = workspace:WaitForChild("CharacterStorage")

local Tool = script.Parent
local Particle1 = Tool:WaitForChild("Hitbox"):WaitForChild("Particle1")
local Particle2 = Tool:WaitForChild("Hitbox"):WaitForChild("Particle2")

local equipped = false
local timer = 0
local maxChargeTime = 2
local charged
local maxDistance = 250

-- Reset script values for next attack
local function resetValues()	
	holdingClick = false
	charged = false
	Particle1.Enabled = false
	Particle2.Enabled = false
	Particle1.Rate = 30
	timer = 0
end

-- Tool equip and unequip events
Tool.Equipped:Connect(function()
	equipped = true
end)

Tool.Unequipped:Connect(function()
	equipped = false
	resetValues()
end)

-- Mouse press events
mouse.Button1Down:Connect(function()
	if equipped == true then		
		holdingClick = true					
		Particle1.Enabled = true
		Particle2.Enabled = true
		
		while holdingClick do
			timer += 1
			task.wait(1)
			
			if timer >= maxChargeTime then
				Particle1.Rate = 80
			end
		end
	end
end)

mouse.Button1Up:Connect(function()
	if equipped == true then
		
		holdingClick = false
		if timer >= maxChargeTime then 
			charged = true			
		end
		
		-- Check to see if the target is in range and isnt the skybox
		if mouse.Target ~= nil and (HumanoidRootPart.Position - mouse.hit.Position).Magnitude <= maxDistance then
			FireballEvent:FireServer(mouse.Hit.Position, charged, Tool)	
		end
		
		resetValues()
	end
end)

This is the server script that receives the remote event call:

Summary
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteEvents = ReplicatedStorage:WaitForChild("RemoteEvents")
local FireballEvent = RemoteEvents:WaitForChild("FireballEvent")

local onCooldown = false
local cooldown = 1

-- Recieve event from local script, then fire back to all clients
FireballEvent.OnServerEvent:Connect(function(player, mousePosition, charged, Tool)
	local Humanoid = player.Character:WaitForChild("Humanoid")
	
	-- Make sure the player isnt dead while firing
	if Humanoid.Health > 0 and Humanoid:GetState() ~= Enum.HumanoidStateType.Dead and onCooldown == false then
		FireballEvent:FireAllClients(player, mousePosition, charged, Tool)

		onCooldown = true
		task.wait(cooldown)
		onCooldown = false
	end
end)

This is the local script that receives the remote event back from the server:

Summary
local Workspace = game:GetService("Workspace")

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteEvents = ReplicatedStorage:WaitForChild("RemoteEvents")
local FireballEvent = RemoteEvents:WaitForChild("FireballEvent")
local DamageHumanoidEvent = RemoteEvents:WaitForChild("DamageHumanoidEvent")

local ProjectileStorage = workspace:WaitForChild("CharacterStorage"):WaitForChild("ProjectileStorage")

local Fireball = script:WaitForChild("Fireball")
local explosionRadius = Vector3.new(12,12,12)
local chargedExplosionRadius = Vector3.new(18,18,18)
local damage = 15
local chargedDamage = 30

local travelSpeed = 60

local TweenService = game:GetService("TweenService")
local tweenLength = 0.8


-- Bezier curve functions
local function lerp(p0, p1, t)
	return p0*(1-t) + p1*t
end

local function quad(p0, p1, p2, t)
	local l1 = lerp(p0, p1, t)
	local l2 = lerp(p1, p2, t)
	local quad = lerp(l1, l2, t)

	return quad
end

-- Create a projectile when the server sends out the event to all clients
FireballEvent.OnClientEvent:Connect(function(player, mousePosition, charged, Tool)
	print("MOOSE!!!!")
	local clone = Fireball:Clone()	
	clone.Parent = ProjectileStorage
	clone.CFrame = Tool:WaitForChild("Hitbox").CFrame
	
	if charged then
		clone.Size = clone.Size * 2
	end
	
	clone:WaitForChild("Fire"):Play()
	
	-- Start bezier curve path
	local randomX = math.random(-12, 12)
	local start = Tool.Hitbox.Position
	local finish = mousePosition
	local middle = (finish + start)/2 + Vector3.new(randomX, 30, 0)

	for i = 1, travelSpeed do
		local t = i/travelSpeed
		local updated = quad(start,middle,finish,t)
		clone.Position = updated
		task.wait(0.01)
	end
	
	-- Disable projectile particles
	for i, effect in pairs(clone:WaitForChild("Attachment"):GetChildren()) do
		effect.Enabled = false
	end

	-- Create tween
	local info = TweenInfo.new(tweenLength, Enum.EasingStyle.Linear, Enum.EasingDirection.In, 0, false, 0)
	local goal = {}
	goal.Transparency = 1

	if charged then
		goal.Size = chargedExplosionRadius
	else
		goal.Size = explosionRadius
	end

	local tween = TweenService:Create(clone, info, goal)
	tween:Play()
	
	local foundCharacters = {}	
	local timer = tick()
	
	repeat
		task.wait()

		-- Get parts in region
		local foundParts = Workspace:GetPartsInPart(clone)

		-- Add characters to table and damage players
		for i, part in pairs(foundParts) do
			local Humanoid = part.Parent:FindFirstChildWhichIsA("Humanoid")

			if Humanoid and not table.find(foundCharacters, part.Parent) and part.Parent ~= player.Character then
				table.insert(foundCharacters, part.Parent)
				
				if charged then
					DamageHumanoidEvent:FireServer(Humanoid, chargedDamage)
				else 
					DamageHumanoidEvent:FireServer(Humanoid, damage)
				end
			end

		end
	until (tick() - timer >= tweenLength)
	
	table.clear(foundCharacters)
	clone:Destroy()
end)

Any thoughts or ideas would help. Additionally, I’ve run into the issue of humanoids being damaged multiple times because each client fires off the “damage humanoid remote event” under the last local script. Anything would be super appreciated, thank you!

2 Likes

The first issue with all clients seeing the magic bullet is caused by this:

FireballEvent:FireAllClients(player, mousePosition, charged, Tool)

FireAllClients sends the event to all clients. You may want to change that to just fire client. You don’t need to change the parameter list, but you will need to change it on the client because player isn’t sent to the client script. So remove player.

FireballEvent.OnClientEvent:Connect(function(player, mousePosition, charged, Tool)

As for your second issue, the cause is that your fireball is striking multiple parts on each player. The Touched/Hit event will fire for each part of the player that the fireball touched. So in your Touched/Hit event handler, you will need to record the player that was damaged in a table and then check the table to see if the player is already on the list. If they are, then ignore the event.

Here’s some pseudocode:

local playerHitList = {}
Hit/Touched Event Handler(function(hitPart)
	-- Find the player
	if playerHitList[player.UserId] ~= nil then
		playerHitList[player.UserId] = true
		damage player
	end
end)

Hope this helps.

1 Like

Hi, thank you for the reply!

I don’t believe that this would work in my scenario as the projectile would only be created on the player’s own screen, while I need every player to be able to see the projectile.

FireballEvent:FireClient(player, mousePosition, charged, Tool)

Additionally, that isn’t the cause of the damage duplicating. A check ensures that the damage only occurs once per attack, which works correctly in a solo server. The damage multiplying is caused by the fact that the client script that creates the projectile also calls the damage humanoid remote event, so for every player that exists, that remote event is called, leading to the damage stacking multiple times over.

I took a second look at your code. In both of your cases, you are going to have to have manage the projectile from the server. From what I see, the client is telling the server to damage the target, and how much damage to apply. This is a BIG security hole that exploiters can use to cheat in your game.

What you can do, as I mentioned above, is manage the projectile from the server. You can use the client to handle any effects that you require through the use of remote events. In my opinion, not only will this solve the security issue, but it should solve your other issues as well.

1 Like

It does seem like that would be the case, I simply attempted to make this client-sided because server-sided looked quite odd with the latency. Ill likely just transfer it over to a server script, though Id still like to know why the replication was happening in the first place.

It’s because the server is responding with FireAllClients. If you look at the client script receiving it, it responds back to the server with the damage info. It works for a single player because there’s only one client. However, when you add clients, they are all going to execute the same code because they are all receiving the same information from the server. Therefore, they are all going to send the same information back to the server. As a result, the damage gets multiplied by each connected client.

Once again, the problem is FireAllClients.

If you really want to have the effect replicated to all clients, then keep the damage calculation logic on the server but then tell the client about the projectile, where it is going, etc. That will keep the effects on the client while maintaining good security and server efficiency.

1 Like