Need help on Client-Sided Replication for VFX

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? I want to reduce the server load by making the VFX client-sided.1) Orig player will create the VFX locally and fire a remote → 2) Server script will detect that and fire to all clients the location of the VFX → 3) Other clients will receive that event and replicate the VFX on the location of the orig player.

  2. What is the issue? I’ve achieved creating the VFX locally for the client but I have no idea on how to replicate the VFX of the local script to the other players. I want 2 scripts only: the local script under the tool (which will handle both the creation of the VFX for the orig player and will also replicate for the other clients) and the server script (which will receive the event fired and fire back to other clients and other clients on their local script will replicate the VFX).

  1. What solutions have you tried so far? I’ve tried creating it server-sided which works but has a high server load. I came across this post but I’m having trouble understanding the logic of how he was able to create this using only the client side script and a server script: Full Beginner's Guide on scripting Anime/Fighting VFX(Visual Effects) [Part 1]

Also, do I need to create an entirely different local script that will handle the VFX replication of different attacks (limited to 1 script only and just sending properly formatted arguments)? I thought of the problem which was the if the player doesn’t have the tool then the local script wouldn’t be with him and won’t replicate the VFX.

Here’s my code:

Client

-- IcePathLocalScript
local UIS = game:GetService("UserInputService")
local CollectionService = game:GetService("CollectionService")
local tool = script.Parent
local player = game.Players.LocalPlayer
local mouse = game.Players.LocalPlayer:GetMouse()
local remote = game.ReplicatedStorage["Attack Related"].RemoteEvents.Ice:WaitForChild("IceMagic")
local isEquipped = false

local origToolName = tool.Name
local COOLDOWN = 2

local debounce = false

-- Function to handle tool being equipped
local function onEquipped()
	isEquipped = true
end

-- Function to handle tool being unequipped
local function onUnequipped()
	isEquipped = false
end

tool.Equipped:Connect(onEquipped)
tool.Unequipped:Connect(onUnequipped)

local function createIcePath(mousePosition, magicType, inputState)
	-- Create the ice path locally for the player who triggered it
	-- You can use your previous code for creating the ice path here
	if magicType == "IcePath" and inputState == "Holding" then
		local icePartTemplate = game.ReplicatedStorage.Items.IceAttacks["Ice Path"]:WaitForChild("IcePathModel")
		local icePartTag = "IcePath"
		local footOffset

		-- Check the player's WalkSpeed and set the footOffset accordingly
		if player.Character.Humanoid.WalkSpeed >= 100 then
			footOffset = Vector3.new(-0.5, 0, 25) -- Offset the ice part 50 studs in front of the foot
		else
			footOffset = Vector3.new(-0.5, 0, 10) -- Offset the ice part 20 studs in front of the foot
		end

		for i = 0, 100 do
			wait()

			local icePart = icePartTemplate:Clone()
			icePart.Parent = game.Workspace.DebrisFolder
			icePart.CanCollide = true
			icePart.Anchored = true
			icePart.CanTouch = true

			local character = player.Character
			local footPosition = character:WaitForChild("RightFoot").Position
			local direction = (mousePosition - footPosition).unit
			local rightVector = character.HumanoidRootPart.CFrame.RightVector
			local lookVector = character.HumanoidRootPart.CFrame.LookVector

			local positionInFrontOfFoot = footPosition + rightVector * footOffset.X + lookVector * footOffset.Z

			icePart.CFrame = CFrame.new(positionInFrontOfFoot, positionInFrontOfFoot + direction)
			icePart.Orientation = Vector3.new(0, 0, 0)
			icePart.Size = Vector3.new(0.001, 0.001, 0.001)

			local newSize = Vector3.new(42.84, 0.2, 40.176)
			local TweenService = game:GetService("TweenService")

			local tweenInfoGrow = TweenInfo.new(
				0.2,
				Enum.EasingStyle.Linear,
				Enum.EasingDirection.Out,
				0,
				false
			)
			local tweenGrow = TweenService:Create(icePart, tweenInfoGrow, {Size = newSize})
			tweenGrow:Play()

			tweenGrow.Completed:Connect(function()
				CollectionService:AddTag(icePart, icePartTag)

				-- New tween for the ice part's duration before shrinking
				local tweenInfoShrink = TweenInfo.new(
					2,
					Enum.EasingStyle.Linear,
					Enum.EasingDirection.In,
					0,
					false
				)
				local tweenShrink = TweenService:Create(icePart, tweenInfoShrink, {Size = Vector3.new(0.001, 0.001, 0.001)})
				tweenShrink:Play()

				-- Add the ice part to debris after it lasts for 1 second
				task.delay(2, function()
					game.Debris:AddItem(icePart, 0.0001)
				end)

				-- Send the ice path's position to the server for replication to other clients
				remote:FireServer(mousePosition, icePart.Position, magicType, inputState)
			end)
		end
	end
end


-- When the client triggers the ice path
local function onIcePathTriggered()
	-- Fire the local function to create the ice path
	createIcePath(mouse.Hit.Position, "IcePath", "Holding")
end

UIS.InputBegan:Connect(function(input, processed)
	if processed then return end

	-- Check if the tool is equipped before taking any input
	if isEquipped then
		-- Check if the left mouse button is pressed
		if input.UserInputType == Enum.UserInputType.MouseButton1 and debounce == false then
			debounce = true

			-- Call the local function to create the ice path
			onIcePathTriggered()

			-- Change tool name to the cooldown amount
			task.spawn(function()
				for i = COOLDOWN, 1, -1 do
					tool.Name = "[".. tostring(i).. "]"
					wait(1)
				end
				tool.Name = origToolName
			end)

			wait(COOLDOWN)

			debounce = false
		end
	end
end)

-- Listen for the server response and create the ice path locally
remote.OnClientEvent:Connect(createIcePath)

Server

-- IcePathServerScript
local remote = game.ReplicatedStorage["Attack Related"].RemoteEvents.Ice:WaitForChild("IceMagic")

remote.OnServerEvent:Connect(function(player, mousePosition, icePartPosition, magicType, inputState)
	if magicType == "IcePath" and inputState == "Holding" then
		-- Replicate the ice path to all other clients
		remote:FireAllClients(player, mousePosition, icePartPosition)
	end
end)

P.S. The icePartPosition arguments aren’t really working because I don’t know how to receive it on the other clients that will replicate it on the same exact position as the player. This is my first time posting so bear with me HAHAAH

1 Like

It is great that you are making your visual effects on the client rather than the server - this is how it should be done, though some creators may feel lazy and do everything on the server instead, which is a no-no :-1:

Without reading your code, the current network system you have is good - you have a client that sends a signal to the server when a VFX needs to be created, and the server sends a signal to the rest of the clients to replicate that locally.

I’ve done a similar system in the past, and the best way to do it is to have 3 scripts: 1 module script, 1 local script, 1 server script (this can vary depending on your preference). The module script will consist of some callable function that creates the VFX on the client. This function will be called when the player needs to create the VFX on their end AND when the player receives a signal from the server to create the VFX (that originated from another player). That way, you won’t need two different scripts to handle the VFX creation, only one!

1 Like

Thanks! I’ve only been scripting for more than a month and yes, this is my very first language to learn, and very first game WAHAHA!

Do you think you could share with me that system? It would be a great help in the future when I add more attacks. That system sounds great!

1 Like

AH! You’re thinking like a pro coder already :sunglasses: I definitely wouldn’t thought of such a complex yet efficient system during my first few months of coding, props to you! :smiley:

Unfortunately I do not have access to this system any longer. If you plan on adding more attacks, I suggest a module named “AttackFVX” and have a list of functions that are callable depending on the name of the attack that is used.

local AttackVFX = {}

function AttackVFX.HulkSmash()

end

function AttackVFX.LaserEyes()

end

function AttackVFX.DropKick()

end

return AttackVFX

Have this module required by your player controller (so that it can call these functions when the player does an attack) and by a remote event listener (could be within the same script, it will call these functions when it receives a signal from the server to replicate the VFX called by another player).

1 Like

hello, thanks for tagging my post on how to script VFX. I’ve read your code and there are a few things I would like to point out. As @Awesom3_Eric has pointed out already, the current network system is solid and should be done that way, but also not in that way lol… you’ll see below.

Let’s get rid of the unimportant but helpful ones as a start.

Firstly, on a server script using :WaitForChild() is unnecessary as you did for locating the “IceMagic” remote; server scripts are run after the game has loaded!
image

Next, since you have already required player, it would be more optimized to just do
local mouse = player:GetMouse()
image

There’s a few more but since I’m a little busy and these won’t affect anything much, lets’s get to the main point shall we?

This part of code right here is supposed to be under the InputBegan connection.

-- Send the ice path's position to the server for replication to other clients
remote:FireServer(mousePosition, icePart.Position, magicType, inputState)

This will send the mouseposition to the server, which the server sends this back to all clients and the function createIcePath() has the mousePosition argument yes? This will make all of the data identical because the position sent by Player1 would be sent to the server and all it does it send that same position to all clients. Then when createIcePath() is called from ,
remote.OnClientEvent:Connect(createIcePath)
all of the clients will have the same replicated vfx at the same position.

local function onIcePathTriggered()
	-- Fire the local function to create the ice path
	createIcePath(mouse.Hit.Position, "IcePath", "Holding")
end

This function is very unnecessary and should be deleted, along with this

-- Call the local function to create the ice path
onIcePathTriggered()

If you have any further questions let me know and I’ll try to reply as soon as possible.

1 Like

I want 2 scripts only

This is a bad idea architecturally since you probably want other clients to be rendering the VFX whether or not they have that specific tool in a backpack where the tool script can run.
You would want to have 3 decoupled systems and 1 shared / utility module:

  • client vfx replicator: script / module / class that is always listening on the client for network events to replicate other players VFX
  • client tool controller: script / module / class that controls the players tool, plays VFX started by the local player and invokes and sends VFX network events to the server
  • client shared VFX code: module with shared functionality (ex: big boom function) that is used by the both the client tool controller and replicator
  • server: script / module / class that recieves updates from client tool controller (one client) and replicates to the replicators (all the other clients)

From my experience, this is pretty much the best way to go. Its worth noting though that a lot of the replication stuff can be abstracted pretty easily over a lot of different effects, so it is fairly common practice to have a central “effect replicator” script that delegates effects for a bunch of different tools.

Alternatively, you could also have the tool controller dispatch to the vfx replicator.

1 Like

Hey! Sorry for the late reply cuz I took a break cuz of an eye infection. So I tried experimenting on my own with a thought I had just earlier and this is the result:

Here’s the heirarchy:
image

-- Local Script (PartCreate)
local UIS = game:GetService("UserInputService")
local player = game.Players.LocalPlayer
local replicateRemote = game.ReplicatedStorage.Replicate

local debounce = false

UIS.InputEnded:Connect(function(input, isTyping)
	if isTyping then return end
	if input.KeyCode == Enum.KeyCode.E and debounce == false then
		debounce = true
		print("firing")
		local icePath = game.ReplicatedStorage.IcePath:Clone()
		icePath.Parent = game.Workspace
		local location = player.Character.HumanoidRootPart.CFrame * CFrame.new(0,0,-10)
		icePath.CFrame = location
		
		script.RemoteEvent:FireServer(location)
		wait(5)
		debounce = false
	end
end)

replicateRemote.OnClientEvent:Connect(function(partLocation)
	print("received input from replicateRemote")
	local icePath = game.ReplicatedStorage.IcePath:Clone()
	icePath.Parent = game.Workspace
	local location = player.Character.HumanoidRootPart.CFrame * CFrame.new(0,0,-10)
	icePath.CFrame = partLocation
end)
-- Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local replicateRemote = ReplicatedStorage.Replicate

script.Parent.OnServerEvent:Connect(function(player, partLocation)
	replicateRemote:FireAllClients(partLocation)
end)

The script is working and I think I’m one step closer to the correct client-sided VFX replication logic. The problem is: for other players, 1 ice path will be created (which is the right one) but for the player who if the one that created the ice path, he will get two (in his workspace). You can see in the video that the amount of icepaths in the workspace aren’t equal and the problem is in the local script.

What should I do? Btw I got that idea from reading your post but I didn’t follow the tutorial yet because my eye got infected and I had to take a break.

Also, an amazing VFX creator I follow just uploaded this today which teaches using module scripts and client-sided replication… but your post is much more detailed <3:

Edit 2 hours later: Some dev. told me that I should put in a second argument (the original player who fired the remote) to be sent by the server to all other clients and use an if statement to compare if the orig player is the current player. If not then it will replicate. in the local script and it worked!

Still need your take on it tho hehe

you were getting on the right track, and this was indeed the solution lol

another way to do it is to remove this

local icePath = game.ReplicatedStorage.IcePath:Clone()
icePath.Parent = game.Workspace
local location = player.Character.HumanoidRootPart.CFrame * CFrame.new(0,0,-10)
icePath.CFrame = location

which is inside of the userinputservice, as well as the checking if original character == current player and thus will spawn the same for everyone, including the local client

As for the hierarchy it’s alright, but I’d recommend a few things that aren’t necessary but could help you,

  1. Try using folders for your skills in ReplicatedStorage, such as “IcePathFolder” and put all your remotes and vfx regarding that into the folder. Helps a lot with being clean and for indexing in the long run
  2. wait() is deprecated, try using task.wait() which has no throttling unlike wait()
  3. Practice making server-sided debounce, one of the best anti-exploits; it disables skill spamming
local replicateRemote = game.ReplicatedStorage.Replicate

try your best to use :WaitForChild() when fetching objects from the local script, as while the script may have loaded, some objects may still be loading. This can cause the script to error and everything below that line of code will not work.

local replicateRemote = game.ReplicatedStorage:WaitForChild("Replicate")

That should be pretty much it. If you have any further questions then fire away! Also, good luck with the eye infection!

1 Like

Update:
All of my attacks are client-sided VFX and I’m on my last set of attacks already nearing my release of the game but I encountered a problem:

The tools used to trigger the attacks have the local scripts under them. I tested a situation where player 1 has the tools and player 2 does not. It didn’t replicate as you might have expected.

So I placed the local scripts instead under the StarterCharacter which worked! But, the problem is when the player dies, the scripts under him will stop when he respawns. This results into some of the debris not being deleted fully since I have tweens before deleting them using the Debris Service inside the local script. So for player 1, the debris is still there but for player 2 it has been removed.

For now I will just make a debrisHandler on the StarterCharacter that will count how long the debris have been in the DebrisFolder and if it’s been there for an unusual amount of time, I will tween and delete it. If it can’t be tweened then I will add it to the debris service immediately.

How to deal with this?

@SushiScripter

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