How we reduced bandwidth usage by 60x in Astro Force (Roblox RTS)

Reading through this thread I see you’ve done a lot of research and testing. I was wondering if it’s even more efficient to send strings. For example, instead of sending a number that might be 32 or 64 bit, why not send a string?:

local number = 123456789
local stringNumber = tostring(number) -- sending this value

local convertedNumber = tonumber(stringNumber) -- convert "stringNumber" once sent

I read on the lua documentation that string are 8-bit so would theoretically help your problem:
“Lua is eight-bit clean and so strings may contain characters with any numeric value, including embedded zeros.”

Do you think that this can be another workaround for reducing bandwidth usage? Note that I’m not familiar with “hashes” or “bytecode” or things like that, it’s just something I thought of that might work. I also might’ve misinterpreted the information in the documentation.

6 Likes

Ooh this is a super interesting idea! I’ll definitely need to try this out :eyes:

2 Likes

Actually, I believe each character is 8 bits, not the whole string. So if you had 123, that’d already by 3x8 = 24 bits.

7 Likes

The jump from gen 1 to gen 2 is something I can strongly relate to. At the beginning of RoKarts, the kart assembly consisted of many welded parts, several constraints (springconstraint and prismaticconstraint mainly) and many moving parts. All of this heavy physics data would get replicated to the server, even though none of it was wanted. I remember having something insane like 60 KB/s send. The story only got worse, as I would have AI racers run on the server, and each AI had its entire kart exist in workspace, physics objects and everything. This meant that the server would replicate all of this physics data back to the clients, resulting in something absurd like 400 KB/s at 6 AIs simulated (and something like 1000 KB/s for the full 11 AIs).

The solution, obviously, was to transition the movement system to something that would require nothing in workspace, so on the server, you could have a perfectly empty environment with just the racetrack itself, and on the client, you would only draw the core ingredients of the karts for visuals sake.

Right now, I have 3 KB/s send, and up to 50 KB/s with a full lobby of 12 players. The baseline for receive is actually 20 KB/s because I need to send some special data back to the client for server authoritative magic. For every additional player in the server, it costs about 8 KB/s. Then you might ask, how do I manage to get just 50 KB/s? If each player costs 8, and there can be up to 11 players, then it should be 108 KB/s right? The trick that I use is, on the server, I do distance checks from each client to every other client. Then, I use this distance information to inform which karts are highest priority to be replicated, and pick those, somewhat like a rate-limit but smarter. I cap the number of karts I can replicate to just 4 per frame, and figure out what are the optimal 4 karts I can send to keep everything looking as smooth as possible. Karts close to you should obviously be updated quicker, while karts far away can be updated only once every 6 frames or slower while being almost imperceptible.

Oh by the way, a cool trick for anyone reading. If you have to represent some sort of state for a player, an object, et cetera, you might end up with a large table of booleans to represent the state. For example, with a character controller, you might have booleans like IsFalling or IsSitting or IsCollidingWall. For these cases, you should definitely make use of bit32 to pack the bools into one or a few characters. You can put 8 booleans into one character! I do believe remotes have some inefficiency in order to communicate what type of data each argument is, so having one giant string to describe everything you need to replicate should be leaner than having the data all separated. At least I saw a fairly significant savings from that.

41 Likes

I wasnt expecting the Optimisation V3 with Bits manipulation ! Thanks a lot for sharing!!

3 Likes

brb stealing this optimization idea

11 Likes

This is very helpful information indeed, but I believe a topic like this belongs in #resources:community-resources.

Great work, though! Thanks!

2 Likes

This is the best post I have ever read on DevForum. Gotta love how you document every detail of your progress since V1 and I gotta say, that is very smart thinking the way you optimized your codes into bits to save time, and data.

I will definitely try your methods for my game with my team since I will be using loads of data.

Keep up the great work Atrazine!

2 Likes

Good to see that posts like these are surfacing. I had a similar scenario, where I ran the simulation of “ai agents” completely on the server with no visual component and sent necessary data over remotes. My experience has been to only send these events when “operator” reacts to state changes instead of sending data every lifecycle/network step. Would you mind sharing what you opted for?

I don’t think you mentioned whether you used humanoids or not, I cannot imagine that you would use them given the scale. And looking at the fact that they’re clothless, also with the developer-implemented collision system, most likely no humanoid? I would like to share that if aesthetic is very important to you and you want clothes on your npcs but do not want to use humanoids, I strongly suggest remapping the UV of the npc’s bodyparts. Now you only need to take the assetid of a shirt and put it directly onto the meshpart’s textureid field. Looks great at the very least :grinning_face_with_smiling_eyes:.

Additionally, I think it is awesome that you are shedding light on the how useful it is to learn binary numbers. 3b1b has tons of videos that explains advanced concepts based on similar mechanics.

5 Likes

Hello!

I’m not 100% what you mean by only sending events when “operator” reacts to state changes. What we do is we only send positioning data for units that are moving. To do this, whenever a unit moves, we mark it as “position changed”. Then, when we perform replication, we replicate units that have the “position changed” marked. We then mark the “position changed” as false again. Hope this answers your first question! :slight_smile:

We do not use humanoids! Everything is custom (custom collisions, raycasts, etc.) for best performance and control over how everything works. The UV map idea you mentioned is definitely something we’d love to do! Thanks for the suggestion, I didn’t know such a thing was possible. :smiley:

I’ll have to check out 3b1b’s videos! I didn’t know they had videos on binary numbers. Ty for the suggestion!

3 Likes

Hahaha if we can’t figure out the UV stuff, I’ll hit you up :slight_smile:

I’ll take a look at his videos! Thanks for the link.

2 Likes

Why are you using an obscure data type to pack your data instead of simply directly sending a binary string which would not only reduce your bandwith usage even further but be faster to run as well as it would simplify your code.
You also run the risk that roblox might change the replication behavior for these data types down the line.

1 Like

Great post! Awesome work on the game so far, it’s looking real cool.

As a couple others have referenced, Roblox backported Lua 5.3’s string.pack and string.unpack in August 2020 (more than a year after my original topic). Combined with table.concat, these functions allow you to easily encode and decode an entire list of values to and from one compact binary string. The savings you would get going from your current setup to this approach are relatively minuscule, but preparing data to go over the wire is perhaps the most appropriate use for string.pack on Roblox right now. it certainly suits the task better than these funny int16 vectors. Nevertheless, I appreciate your appreciation of my wild hack :stuck_out_tongue:

7 Likes

Other people have mentioned using binary packing in lieu of a Vector2int16 but for your case that would actually up the bandwidth! The footprint of a string is 5 + n bytes (where n is the length of the string) compared to a set 5 for Vector2int16.

In your case, if you changed nothing else, you would end up with a 7 byte payload if you sent a string… At which point you may as well just send the number outright.

3 Likes

This is only true for very small packets in which case bandwidth wouldn’t be an issue anyway.
For large payloads assuming your own numbers are correct if you were to send for example a packet with a size of 300 bytes you would end up with a bandwidth usage of 305 bytes for a binary string, 350 bytes if you are sending an array of 50 Vector3Int16 and 375 bytes if you are sending an array of 75 Vector2Int16 plus some additional overhead due to sending an array instead of a single string.

2 Likes

I didn’t think of that! Definitely something I’ll have to try out! :slight_smile:

Thanks! I’ll definitely check out the string thing soon too :smiley:

I use to do something like this. Someone told me it was bad practice, which didn’t make sense. But reading your topic, I may come back to this and redo my frameworks to include byte channel streams to manage.




Obviously still some bad practices in regards to how some packets are being used lol but the main idea behind byes is here I think?

Correct me if I’m wrong because I don’t want to backtrack

Also try and ignore all those bad requires. This was very early code practice on the Roblox platform.

Just want to validate that the pattern I had is in line with this article.

Thanks for the read

4 Likes

At first glance, looks good! As long as it’s saving bandwidth :smiley:

2 Likes

Yes that’s why I said “in [the OP’s] case”. Obviously the math works out to being more efficient the more data you add - that’s very basic math. It’s more efficient for every case but one where you’re sending a single Vector2int16… Which is what the OP seems to be suggesting that they’re doing.

Given that, it seems pertinent to mention it.

1 Like