Constant Replication without excessive Remote Calls (REDOING)

Introduction

In my (first) tutorial, I picked this topic because it really seems to be looked over as a cardinal sin by many developers. I have seen many uses (including within some popular games) of a painful amount of Event spam (i.e. firing an event on every mouse move). This is bad practice for a number of reasons, including:

  • Gives room to server lag - While doing this doesn’t inherently always create additional lag (despite the spam requests often occupying both client and server resources), it allows for the server to ‘choke’ on the requests coming through (some requests take longer to go through due to network speed fluctuation, request size etc) and generally become de-synchronised (if you are rapidly firing events, it will make the movement you want to achieve generally choppier and will have a very noticable delay)

  • Often illudes to an insecure game - Firing events rapidly often means that you have some sort of wild issue with your Client-Server Model (read more about that here). This means it’s more likely for an exploiter to be able to find vulnerabilities in your code.

  • Inefficient - 90% of the time, there is a better way to replicate something to others without the use of RemoteEvents. This is what I hope to inform you of later on.

So how do I go about fixing this?

It’s actually fairly simple. There are two main options you should consider, both have different uses and reasons as to why you may or may not use them. Anyway, without further or do here are the explanations of both:

Method A: Local Replication
This is an interesting method which encompasses two different topics: Remote Events, and Local Parts. Both have good wiki tutorials which I have linked.

So, what is the use of locally replicating parts? Well, the answer is generally when you need to have something replicate, but you don’t want the user to have full control over it. An example of this is a block bullet. If you use Method B (Discussed later), you will end up giving the sending user complete control over where that bullet hits. They could teleport it to any player instantly, and do that player damage without even aiming.

Anyhow, here is the quick diagram I have made for this, using the Bullet analogy as an example:

Now, onto actually coding it. Firstly, we need to write the Server that routes all the requests so that it can be handled by each client. This should be in ServerScriptService.

I wrote some demo code for this here:


local RemoteEvent = Instance.new("RemoteEvent")
RemoteEvent.Name = "BulletReplicator"
RemoteEvent.Parent = game.ReplicatedStorage
--Creating a new RemoteEvent called 'BulletReplicator' in the ReplicatedStorage Service.

RemoteEvent.OnServerEvent:connect(function(Player,HitLocation)
	if not HitLocation  --If no Hit Location (Player Mouse location)
		or not Player.Character --Or the Players' character doesn't exist
		or not Player.Character:FindFirstChild("Weapon") --Or the player isn't holding a weapon
	then return end --Then don't procede from here
	--Otherwise, do this:

	for _,NetworkPlayer in pairs(game.Players:GetPlayers()) do
		if Player  ~= NetworkPlayer then
			RemoteEvent:FireClient(NetworkPlayer,Player.Character:FindFirstChild("Weapon").Handle,HitLocation)
		end
	end
	--Fires every client with the Player Name, The Origin and the Target.
end)

Next up is the receiving client. This should always and only be in the StarterPlayerScripts.
Here is the demo code for that:

local RemoteEvent = game.ReplicatedStorage:WaitForChild("BulletReplicator")
--Wait for Bullet Replicator Event in ReplicatedStorage

RemoteEvent.OnClientEvent:connect(function(OriginInstance,HitVector) --This function is called when RemoteEvent::FireClient is called on the player.
	--[[
		Insert Bullet Creation & Damage Code Here from OriginInstance.Position and HitVector
	--]]
end)

Finally, we need the tool that does the initial request. The code for that is here:

local Player = game.Players.LocalPlayer
--Declare Player for easier access
local Mouse	 = Player:GetMouse()
--Declare Mouse for easier access
local Tool   = script.Parent
local RemoteEvent = game.ReplicatedStorage:WaitForChild("BulletReplicator")
--Wait for Bullet Replicator Event in ReplicatedStorage

Mouse.Button1Down:connect(function()
	if not Tool.Parent == Player.Character then return end -- Check if Tool is equipped (without Events)
	local HitPosition = Mouse.Hit.p -- Declare Hit Pos so that there is no change in value
	RemoteEvent:FireServer(HitPosition) -- Fire Server
	--[[
		Insert Bullet Creation & Damage Code Here from OriginInstance.Position and HitVector
	--]]
end)

WARNING: THIS IS NOT ALWAYS THE MOST EFFICIENT WAY AND CAN CAUSE A NOTICEABLE DE-SYNCHRONISATION BETWEEN DIFFERENT CLIENTS

Now, with that over, we move onto:

Method B: Network Ownership

Network Ownership is a very interesting topic in itself. The Wiki will do a far better explaination than I can do here, however the tl;dr is that you can control certain properties of unanchored parts (i.e. CFrame) if the server grants you permission.

Why is this useful? Mostly for these two main reasons:

  • Other clients are blind to anything you do clientside until replicated - this means that until you tell them, clients have no idea what you are doing (mouse position, keys down etc) until they are told. With this, they can stay blind to your precise inputs and instead just see what is going on.

  • The client has complete control over the property without any lag - even though this can be a vulnerability in some cases (discussed in Method A), it can also be a great advantage. An example of this would be moving a part towards the mouse. Other players have no idea where the mouse is, and as discussed in the Introduction spamming an event every time the mouse moves is inefficient.

Changing a Part’s Network Owner can be done via this method:

Part:SetNetworkOwnership(PlayerInstance NewNetworkOwner)

Note: All character limbs are already network owned by the Player - or you couldn’t move around or play animations!

For the example script, we will be making a part which will travel across the map. In order to do this we will need a RemoteFunction, a Server Script and a LocalScript.

Here is the demo code for the Server Script, which again should always be in ServerScriptService:

local RemoteFunction = Instance.new("RemoteFunction")
RemoteFunction.Name = "NetworkReplicator"
RemoteFunction.Parent = game.ReplicatedStorage
--Creating a new RemoteFunction called 'NetworkReplicator' in the ReplicatedStorage Service.


function RemoteFunction.OnServerInvoke(Player)
	--When RemoteFunction::InvokeServer is called on the RemoteFunction by a client
	if not Player.Character then return end --We need a character to do this!
	local Part = Instance.new("Part") 
	Part.Parent = Player.Character --Create an Anchored Part in the Players' Character
	Part.Anchored = true
	Part.CFrame = Player.Character.Torso.CFrame * CFrame.new(0,0,-3)
	--Set the Part's CFrame to 3 studs infront of the Players' Character CFrame.
	Part:SetNetworkOwner(Player)
	--Set the network owner to that player
	return Part
	--Return that to the client
end

Next up is the client. This should be in StarterCharacterScripts or PlayerGui. Code:

local RemoteFunction = game.ReplicatedStorage:WaitForChild('NetworkReplicator') --Wait for RemoteFunction
local Player = game.Players.LocalPlayer --Declare player for easy Access
local Character = Player.Character or Player.CharacterAdded:wait()  --Either index Players' Character or wait for it to be added

while wait(3) do --Do this every 3 seconds:
	pcall(function() -- Basically means that the while loop doesn't wait for the for loop (making it run even if the for loop is still running)
		local Part = RemoteFunction:InvokeServer() --Call RemoteFunction::InvokeServer on the RemoteFunction
		for i = 1,50 do --Do this 50 times before stopping:
			Part.CFrame = Part.CFrame * CFrame.new(0,0,-3) --Move the part 3 studs infront of its previous position
			wait()
			--Wait for ~1/15th of a second
		end
	end)
end

We end up with this result:
https://gyazo.com/6f873f3b84eca51d9abd31758809f41b

WARNING: THIS CAN HAVE BAD CONSEQUENCES ON YOUR GAME IF NOT USED PROPERLY! ALWAYS CONSIDER EVERY ANGLE BEFORE IMPLEMENTING!

Anyway, thank you for reading my first tutorial. I’d really appreciate some feedback on anything like grammar, layout or readability. I hope you found this useful!

34 Likes

In example A, wouldn’t it be more efficient to let the server send data from Player A to all players but Player A? Player A doesn’t need to know that it shot a bullet, because it already knows.

The only reason I see to send Player A about their action is when the action failed or a result of the action occurred.

Actually, since :FireAllClients is faster due to it being a built-in method, it’s just easier to check if the sending player is local player (on the client script)

Your diagram seems incorrect. First off why would you handle hit detection and health on client B instead of the server or client A. Second off, hit detection should be from client A because of latency and if done on the server or client B there will be a delay or the bullet may not hit at all. Third, health should be calculated from the server with sanity checks as to where client A’s bullet hit and where client B is located.

1 Like

There’s nothing wrong with setting it locally also, the server will correct if necessary and this leads to quicker feedback. (i.e. when player A shoots player B and hits locally, he sees Player B’s health going down immediately, and if the hit event was bogus then the server will not update the health server-side. Then what Player A observes to be Player B’s health will eventually be corrected by the server.)

If you don’t do that, you’d only see the health actually going down 50-200ms (on a stable network) later or so after you hit locally, which isn’t huge but not unnoticeable either.

1 Like

If you handle it on the server though you can prevent kill trading from happening.

I don’t think you’re understanding what I meant: the idea is that you just set the health of Player B on the client of Player A immediately. This change won’t propagate to the server (only the server is in control of health still), but the client of Player A will see feedback immediately. It doesn’t make the game behave differently, it just gives feedback to shooters earlier.

The topic I covered in this tutorial was stopping people from being dumb and abusing remotes, not securing it. It’s up to them to not copypaste my code and then fault me if exploiters hack their place.

That’s not what the diagram shows (ツ)_/¯

It would set the health of player B on only player B since it is local

Because I was adding a comment to a point you were making, not to the diagram (I quoted a sentence of yours, see above). Either way, I think we’re talking about similar things and just explaining it differently.

2 Likes

Can anyone confirm this as I would assume it’s slower as it’s having to network to one extra client?

Server efficiency isn’t based on how many clients are being invoked, especially if it’s just one user.

I don’t understand, even if it’s just one extra client it has to send the data to, that’s more data being sent from the server and more data being received by that client. If the original client in this case has bad internet, that extra networking could make a difference?

1 Like

Network bandwidth is much more valuable than CPU time.
Always look for ways to minimize the amount of information you send and the number of recipients you send it to.

3 Likes

I still prefer the method I use at the moment for the sake of simplicity, and for the sake of a tutorial that should really be a priority.

Just as simple to make a function that excludes a player:

function FireExcluding( RemoteEvent, Plr, ... )
    local Plrs = game.Players:GetPlayers( )
    for a = 1, #Plrs do
        if Plrs[ a ] ~= Plr then RemoteEvent:FireClient( Plrs[ a ], ... ) end
    end
end
1 Like

To be fair the title of the tutorial is “Constant replication without excessive remote calls”, so maybe this change should be made :grin:

1 Like

:stuck_out_tongue_winking_eye: yeah alright i concede defeat

You’re not firing it with the NetworkPlayer as the first argument

Also, if you’re worried about speed don’t use pairs/ipairs as it’s slower then just for i = 1, #x do

Yeah, I only just woke up when I wrote that :stuck_out_tongue: thanks for pointing out

Also again, I’ll keep it using the pairs() function as it is easier for people to understand