Client-Side Replication with Server being able to access Positions

Hello everyone! :star:

I’ve researched a lot about Client-Side Replication in ROBLOX Studio, and i haven’t come across any systems that replicate things to the client, while the Server still actively knows the updated positions of each replicated instance.

I’ve thought of a few things but a lot of them just introduce the same problem that a replication system is used to fix, lag. :robot:

I would really appreciate it if someone could clarify, or even point out that it’s extremely simple and something went over my head, or that it’s pretty complicated and could give me a concise explanation on how i could achieve it.

Thanks for reading! :star_struck:

1 Like

Each frame, you need to send the current object’s position to the server and store it in a table.
I recommend doing this 20 times per second.
You can also update the position on the server only when it changes on the client, but there will always be some latency.

Also, I forgot to mention: you can use buffers to compress data and reduce accuracy to save more bandwidth.
If your objects are within the 16-bit range, you can use Vector3int16.

First off thanks for the response, wouldn’t it be performance heavy though?

I mean i would still be doing the CFrame / Position calculations on the server.
I don’t really care about the latency since i don’t need it to be 100% accurate.

Also wouldn’t it be in reverse? The server would tell the client to update the position?

Made a system really quick, and did a stress test with 1000 parts (My game can definetely reach this given the correct circumstances). All sending compressed string CFrames, (From 12 chars to 6 chars) to all Clients.

And i’ve got this result:

It is not the most optimized LOL.
Even though it is pretty simple:

My current data structure being sent to the Client is:

{
 ["Instance_Name;ID"] = "Compressed CFrame" --3 chars for Position and 3 for Rotation
}

And then 20 times a second i send this compressed data to the Client:

function Replication_Service:Start(): ()
	
	while task.wait(TIMES_PER_SECOND_TO_SEND_INFO_TO_CLIENTS) do
		local INSTANCES_TO_MOVE = {};
		
		for Index, Instance_Table in ipairs(Replication_Service.Instances_To_Replicate) do
			INSTANCES_TO_MOVE[Instance_Table.Instance_Name..";"..Index] = Get_Compressed_CFrame(Instance_Table.Current_CFrame);
		end
		TEST_REMOTE:FireAllClients(INSTANCES_TO_MOVE);
	end
end

Client-side Code:

REPLICATED_STORAGE.TEST_REMOTE.OnClientEvent:Connect(function(Instances_To_Move)
	local OBJECTS_TO_MOVE = {};
	local CFRAME_LIST = {};
	
	for Instance_Name, Current_CFrame: string in pairs(Instances_To_Move) do
		local PART_TO_MOVE = REPLICATED_OBJECTS_FOLDER:FindFirstChild(Instance_Name) or REPLICATED_STORAGE[string.split(Instance_Name, ";")[1]]:Clone();
		if not PART_TO_MOVE.Parent then PART_TO_MOVE.Name = Instance_Name; PART_TO_MOVE.Parent = REPLICATED_OBJECTS_FOLDER end;
		
		table.insert(OBJECTS_TO_MOVE, PART_TO_MOVE);
		table.insert(CFRAME_LIST, From_Compressed_CFrame(Current_CFrame));
	end
	if #OBJECTS_TO_MOVE > 0 then
	   workspace:BulkMoveTo(OBJECTS_TO_MOVE, CFRAME_LIST, Enum.BulkMoveMode.FireCFrameChanged);
	end
end)

In the client for optimal performance i also use workspace:BulkMoveTo();
Also just to simulate some stuff here’s what i do to make the Parts move:

for i = 1, 1000 do
	table.insert(REPLICATEDS, CFrame.new(0, 25, 0) * CFrame.Angles(math.rad(90), math.rad(90), math.rad(90)))
	REPLICATION_SERVICE.Add_To_Replication("TEST_PART", CFrame.new(0, 25, 0) * CFrame.Angles(math.rad(90), math.rad(90), math.rad(90)));
end

while task.wait() do
	
	for ID, Given_CFrame in ipairs(REPLICATEDS) do
		REPLICATEDS[ID] *= CFrame.new(math.random(), math.random(), math.random()) * CFrame.Angles(math.random(), math.random(), math.random());
		
		REPLICATION_SERVICE.Update_Instance_CFrame(ID, REPLICATEDS[ID]);
	end
end

EDIT: Even with 100 parts i get 100 kB, would i need to use buffers to make this viable?

I have started using a module to compress data automatically called Packet, turns out i was only running 500 parts and not 1,000 on my first stress test mb, and now with all the new improvements i halved the kB amount from 2,000 (1K Parts) → 1,000 (still 1K Parts).

It’s the same code as before but this module has literally halved the amount of data sent.
I don’t know if there is any other optimizations i can make :pensive_face:

There is a very good article on the topic of bandwidth optimization. I personally found it very useful and used many tips from it in production.
Link: How we reduced bandwidth usage by 60x in Astro Force (Roblox RTS)

Thanks for the reply! I’ll make sure to check it out, forgot to an update but now i got my optimizations up to 80 kB per 1,000 parts.

I had to sacrifice a few things but this is looking good

1 Like

First off, thanks for all the replies! They really helped in making my Replication System which is now running 2,000 parts with 10 kB/s and pretty decent Server time:

I am marking my message as a solution so anyone who comes across this topic sees it immediately, but all the credit goes to the people that replied to help me develop this System, now i’m going to break down how i really did it, some downsides of the system and upsides:

:up_arrow:
Upsides:

  • This system can run upwards of 10,000+ Parts and still be under 50 kB/s (The only limiting factor here is the CPU / Server, because you have to use a for loop to update each Object’s position every task.wait() / Heartbeat

  • The system uses buffers, a community module called “Packet”, workspace:BulkMoveTo and Lerping as it’s main functions. (Very easy to use)

  • Pretty scalable and is very easy to add new systems / functions to it.

:down_arrow:
Downsides:

  • The system runs with 5 FPS , meaning the objects get replicated 5 times per second, which makes it seem "laggy", but this is disguised using Lerping, and most of the times it’s either unnoticeable or just doesn’t matter.

  • This system does not manipulate the Rotation of the Objects being replicated, meaning if you want to do that you’d have to either add it into the System itself or have the Client do that for some of the Parts you add in.

  • Every Object is lerped with the same speed as to the Server replicating Objects. (1s / FPS) (Again this is can be easily changed by just doing custom speeds on the client or passing them in with each Object.

  • Every Object is always replicated, even if they aren’t moving.

So with all that sorted let’s get into how this System is made:

First off in the Server we made a new ModuleScript and make a variable for the Packet Module, which you can find here: Packet Module

After this we will make a new Packet Object which basically acts the same way as a Remote Event, but instead of sending info normally, it compacts the data using buffers, which will reduce the amount of data being sent by the Server to the Client, next up the second Parameter (after the name of the Packet Object), we will insert a table of Buffers as our type that will be sent to all the Clients:

local PACKET = require(game:GetService("ReplicatedStorage").Packet);
local REPLICATION_PACKET = PACKET("Replication", {PACKET.Buffer}); -- Packet Object

After this we make a Module variable that will store all Objects that will be replicated to all Clients:

Replication_Service.Instances_To_Replicate = {} :: {[number]: buffer};

After doing that we make a Start() function which when executed will do this code:

function Replication_Service:Start(): ()
	
	while task.wait(TIMES_PER_SECOND_TO_SEND_INFO_TO_CLIENTS) do
		REPLICATION_PACKET:Fire(Replication_Service.Instances_To_Replicate);
	end
end

This code will replicate all Objects to every Client every set X amount of seconds which is this calculation: 1 / FPS | Ex: 1 / 20 = 0.05s

So before we continue now let’s get into the compression of Data and how we will handle that:

We will send 7 bytes of data per Object, these 7 bytes amount to 4 variables:

Instance Name: number, X Position: number, Y Position: number, Z Position: number

Now to compress this data to 7 Bytes we will use the buffer Library.
I will get to why the Instance Name is a number and not a string, here is a sample code of how i did the compression of data:

local function Get_Compressed_Data(Instance_Name: number, Given_CFrame: CFrame): buffer
	local Compressed_Buffer = buffer.create(7);
	buffer.writeu8(Compressed_Buffer, 0, Instance_Name);
	buffer.writei16(Compressed_Buffer, 1, math.round(Given_CFrame.X));
	buffer.writei16(Compressed_Buffer, 3, math.round(Given_CFrame.Y));
	buffer.writei16(Compressed_Buffer, 5, math.round(Given_CFrame.Z));
	
	return Compressed_Buffer;
end

I am using 16 bit integers just so this is more scalable with higher scale projects, you can lower this to 4 bytes easily but it’s very dependant on what you are going to do with this System, since the limitations of using only 4 bytes is a lot.

Here is also the Function to extract the Compressed Buffer Data:

-- Client Side:

local function Extract_Compressed_Data(Compressed_Buffer: buffer): (number, CFrame)
	return buffer.readu8(Compressed_Buffer, 0), CFrame.new(buffer.readi16(Compressed_Buffer, 1), buffer.readi16(Compressed_Buffer, 3), buffer.readi16(Compressed_Buffer, 5));
end

-- Server Side: 

local function Extract_Compressed_Data(Compressed_Buffer: buffer): (number, CFrame)
	return CFrame.new(buffer.readi16(Compressed_Buffer, 1), buffer.readi16(Compressed_Buffer, 3), buffer.readi16(Compressed_Buffer, 5));
end

Ok so how do we add Objects to replicate using this System? Here’s a function for that:

--[[
  This Function takes is extremely simple, and inserts the compressed data returned by
  the Compressed Data Function into the Module Variable we made earlier.

  It also uses a Highest Index variable to keep track of all the Objects so they don't
  get out of sync inside the Table, if you were to remove one of them from it.

  If you want to make a Function to remove an Object from Replication it's extremely 
  simple, all you gotta do is do the same as this Function but in reverse, while
  sending an event to all Clients to Destroy() the Object that is not being replicated.
]]
function Replication_Service.Add_To_Replication(Instance_To_Replicate: number, Starting_CFrame: CFrame): number
	HIGHEST_INDEX += 1;
	table.insert(Replication_Service.Instances_To_Replicate, Get_Compressed_Data(Instance_To_Replicate, Starting_CFrame));
	
	return HIGHEST_INDEX;
end

For optimal performance this System does any necessary Code outside of the Start() Function.
This makes it so we do not lose any performance by doing unnecessary things every time we
replicate Objects.
Now to update the CFrame / Position of a Replicated Object, all we need to do is this:

function Replication_Service.Update_Instance_CFrame(Instance_Name: number, Instance_ID: number, New_CFrame: CFrame): ()
    -- Sanity Checks
	if not Replication_Service.Instances_To_Replicate[Instance_ID] then
		if RUN_SERVICE:IsStudio() then warn(`No Instance Object for Instance ID: {Instance_ID} was found!`) end;
		return
	end
	if not Instance_Name then
		if RUN_SERVICE:IsStudio() then warn(`No Instance Name for Instance ID: {Instance_ID} was provided!`) end;
		return;
	end
    -- Keep in mind the Instance_ID Variable can be acquired via the Add_To_Replication Function.

	Replication_Service.Instances_To_Replicate[Instance_ID] = Get_Compressed_Data(Instance_Name, New_CFrame);
end

And that is the Server-side done, extremely simple, now let’s get to the Client-side:

We will start with doing the exact same thing as in the Server, grabbing the Packet and the Packet
Object variables.

After this we will make 2 new variables:

local MOVE_CONNECTION: RBXScriptConnection = nil; -- Connection for Lerping
local REPLICATED_OBJECT_STORAGE_FOLDER = REPLICATED_STORAGE.Replicated_Object_Storage; -- Folder where all Replicated Objects are stored.

And then 2 Setting Variables:

local MOVE_TIME = 1 / 5; -- This needs to be the same value as the Server's "TIMES_PER_SECOND_TO_SEND_INFO_TO_CLIENTS" Variable
local INSTANCE_NAMES = { -- This is where we get the Object's name, using the Instance_Name Variable from the Server.
	[1] = "TEST_PART";
};

After this we connect to when the Server sends us the Replicated Objects:

REPLICATION_PACKET.OnClientEvent:Connect(function(Instances_To_Move)
	local OBJECTS_TO_MOVE = {};
	local CFRAME_LIST = {};
	
	for Instance_ID: number, Data: string in ipairs(Instances_To_Move) do
		local Instance_Name, Grabbed_CFrame = Extract_Compressed_Data(Data); -- Convert Compressed Data.
		Instance_Name = INSTANCE_NAMES[Instance_Name]; -- Grab the Instance's Name.
		local PART_TO_MOVE = REPLICATED_OBJECTS_FOLDER:FindFirstChild(`{Instance_Name};{Instance_ID}`) or REPLICATED_OBJECT_STORAGE_FOLDER[Instance_Name]:Clone();
		if not PART_TO_MOVE.Parent then PART_TO_MOVE.Name = `{Instance_Name};{Instance_ID}`; PART_TO_MOVE.Parent = REPLICATED_OBJECTS_FOLDER end;
        -- Grab the Part that we will move, and insert it into a Table.
		
		table.insert(OBJECTS_TO_MOVE, PART_TO_MOVE);
		table.insert(CFRAME_LIST, Grabbed_CFrame);
	end
	if MOVE_CONNECTION then MOVE_CONNECTION:Disconnect() end; -- Disconnect any previous Lerps.
	
    -- If we have any Objects to Lerp, do so.
	if #OBJECTS_TO_MOVE > 0 then
		local Alpha = 0;
		local MOVE_PARTS = {}; -- Start Position of all Objects that we will move.
		local CFRAMES_TO_MOVE_TO = {}; -- End Position of all Objects that we will move.
		for _, Object in ipairs(OBJECTS_TO_MOVE) do
			table.insert(MOVE_PARTS, Object.CFrame);
		end
		
		MOVE_CONNECTION = RUN_SERVICE.Heartbeat:Connect(function(deltaTime)
			CFRAMES_TO_MOVE_TO = {};
			Alpha = math.min(Alpha + deltaTime / MOVE_TIME, 1); -- Calculate our Alpha.
			for i, Object: BasePart in ipairs(OBJECTS_TO_MOVE) do -- Loop through all Objects an d calculate their next Position, and insert it into a Table.
				table.insert(CFRAMES_TO_MOVE_TO, MOVE_PARTS[i]:Lerp(CFRAME_LIST[i], Alpha));
			end
			workspace:BulkMoveTo(OBJECTS_TO_MOVE, CFRAMES_TO_MOVE_TO, 
Enum.BulkMoveMode.FireCFrameChanged);
            -- Move all Objects all at once, for optimal performance!
			
            -- If we have finished, disconnect!
			if Alpha == 1 then
				MOVE_CONNECTION:Disconnect();
			end
		end)
	end
end)

And that is the whole System, easy to use and to implement.

If anyone has improvements that they can make to this System, please let me know!
And for anyone that will use this System, no need to thank me :laughing: .

4 Likes

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