For those currently making / maintaining RTS games, you should genuinely consider using BulkMoveTo() for CFraming NPCs and the buffer library (e.g. buffer.writei16()) for compressing data to be passed through RemoteEvents.
BulkMoveTo() → for CFraming NPCs
-- instead of rendering npcs like this
for i,v in npcList do
local npcPart = ...
local npcCFrame = ...
npcPart.CFrame = npcCFrame
end
-- you can use this instead
workspace:BulkMoveTo({npcPart1, npcPart2, ...}, {npcCFrame1, npcCFrame2, ...}, Enum.BulkMoveMode.FireCFrameChanged)
Doesn’t look clean, but it increased my FPS by 70% when attempting to render 1000 NPC (HumanoidRootParts) on the client. Not a serious benchmark so you can take this with a grain of salt, but the difference was noticeable, especially on such a large stresstest as 1000 NPCs.
This function moves a table of BaseParts to a table of CFrames without necessarily firing the default property Changed events. This provides a very fast way to move large numbers of parts, as you don’t have to pay the cost of separate property sets for each individual part.
https://create.roblox.com/docs/reference/engine/classes/WorldRoot#BulkMoveTo
I believe the performance gain is because all the CFrames are changed, in a single batch, through C++ directly. The documentation is a bit vague on what it does exactly behind the scenes, but if you are willing to exchange code readability for performance, genuinely consider using this.
buffer library → for compressing data through RemoteEvents
Let’s say you’re sending out an array that contains information about an NPC, and you’re sending out the X and Z of its position as two seperate values. Let’s focus on those two.
local replicationEvent = RS.Remotes.NPCReplication
game:GetService("RunService").Heartbeat:Connect(function()
replicationEvent:FireAllClients(
{
"123456",
-50, -- 8 bytes + overhead
25 -- 8 bytes + overhead
}
)
end)
By default, Roblox assumes that numbers are 64-bit (8 byte) values, meaning they contain 64 bits or 8 bytes of data. This can get you as high as 18 quintillion, yet realistically speaking, most of those bytes will be generally unused because our position values will never go up that high.
To further showcase what I mean, a signed 16-bit (2 byte) value already has a range of [-32768, 32767], saving over 6 bytes of data (as compared to a 64-bit value) that will be unused anyhow.
Through the buffer library, we can store our X and Z as 16-bit values:
game:GetService("RunService").Heartbeat:Connect(function()
local xBuffer = buffer.create(2) -- specify a size of 2 bytes
buffer.writei16(xBuffer, 0, -50)
local zBuffer = buffer.create(2) -- specify a size of 2 bytes
buffer.writei16(zBuffer, 0, 25)
replicationEvent:FireAllClients(
{
"123456",
xBuffer, -- 2 bytes + overhead
zBuffer -- 2 bytes + overhead
}
)
end)
Once the buffer has been collected by the client, the value can be returned like this:
replicationEvent.OnClientEvent:Connect(function()
local xBuffer = ...
local zBuffer = ...
buffer.readi16(xBuffer, 0) -- -50
buffer.readi16(zBuffer, 0) -- 25
end)
Keep in mind that this is a very simplified overview over the buffer library, I only want to showcase how one could compress data through buffers. And yes, this is like how Vector2int16() specifically uses 16-bit values instead of the regular 64-bit.
The buffer library allows you to handle 8-bit, 16-bit, 32-bit, and 64-bit (heck, even individual bits if you want). Here’s the buffer documentation if you’re interested, but I recommend watching a youtube tutorial for it.
One last thing, if you’re interested in tracking down the exact network bandwidth your RemoteEvents send, use the Packet Profiler plugin. This is how it looks like:
