Custom Character Replication Research

Inspired by @tyridge77’s custom character replication system in The Wild West, I wanted to create such a system for myself as it would have countless advantages and can be used in nearly any game. The concept is self-explanatory: the client sends the server replication information regarding their character, the server validates the data to rule out foul play, the server saves the validated information and updates the player’s location/actions depending on how you choose to use the information. First, let me explain the why behind this.

The advantages are clear. Unlike the vanilla replicator, you have actual control over the replication and can change how it interprets data while blocking any character-related cheating without it even having a chance of occurring (teleporting, speed, noclip, flying, etc). It also has other uses like making it easy to spectate the player’s camera/mouse movements, like in The Wild West. Another consequence of the custom replicator is that cheaters will be completely stumped. They cannot use standard cheats that abuse the vanilla replicator (like the ones mentioned above) and will likely leave; they have little motivation to work on a complex tool that works on only this one game, which I don’t think is even possible due to the server validation.

This all sounds well and good, but the problem lies with the how. Before tackling such a project, I would need lots of information about best practices in sending massive amounts of data through the client-server barrier.
Should you send JSON or just the table straight-up? How important is it to keep the JSON compressed? How should you establish the client-server connection, through ordinary remote events or is there some more sophisticated method I’m not aware of? What is the cap that I should respect when it comes to remote spamming?

Although Tyridge can answer most of these, he has not been active lately and isn’t available, hence this post. Be sure to let me know of what you think of this kind of system, aside from the questions above. Obviously it works well judging off of The Wild West, but could there be hidden downsides?

3 Likes

RemoteEvents/Functions are the best way to send data, yes.

This will never be as fast as ROBLOX’s code, but you could probably cheat and get OK results. Sounds like a fun project.

That’s the trick: don’t send massive amounts of data :slight_smile: What do you really need to represent player movement? A bitmask for inputs, current position + velocity? That’s not too much. Check out the bit32 library for packing more data into uint32s than you could in a JSON. Also see string.pack.

All of this would require you to test performance – is the lower net traffic from packing the data into a binary string worth the cost of the packing? Is JSONEncode so optimized that it doesn’t matter if you’re sending binary data or not?

Anyways, this is a hard problem to solve, which is why the ROBLOX is great in the first place (it solves it “pretty well” for most cases). Solving it efficiently and well with Lua (even Luau) is harder. Especially when you don’t have access to unreliable comms (UDP).

If you don’t need physics, it’s easier.

Some resources:

tl;dr: Unless ROBLOX gives us UDP remote events, this is gonna be almost impossible :slight_smile:

5 Likes

The Wild West proves this otherwise and that’s why I want to try it. I know that they employed lossy number compression in order to reduce number length to only 3 characters, and I know how they scaled the resulting character CFrame, but that’s all I know.

As for the data format, there is a reasonable limit as to how far you can go with it. JSON is pretty much required because you will need to send way more data than just a position. In TWW’s case, you have to send the direction the player is facing, their actions, their current animation (which they have control over), their camera CFrame, mouse position, etc. This lot of information has to be packaged neatly into a dictionary in order to be usable, unless you’re a masochist.

For physics, physics should be calculated locally and verified server-side. This would be the easiest solution. However, if you want to make a game where the character doesn’t obey standard physics (like an orbital RPG), it wouldn’t be too hard to implement the physics locally and, again, have the server verify.

1 Like

I wouldn’t send the JSON, then you have the overhead of the keys, strings, encoded numbers, etc.

Luau just backported string.pack/unpack from Lua 5.3, so binary-encoding data shouldn’t be too bad.

1 Like

Ooh sounds interesting. I’m not familiar with the different formats, which one is most preferable for universal use? If that is impossible what format is useful for what purpose? How do I check it’s effectiveness?

Well, first I would try to get something working with JSON first honestly. I know I said not to… but the hard part of this is going to be correctly syncing up movements at all.

Then, once you get it working with JSON, you can look into places to optimize, like using string.pack. As for the formats, there’s no universal format. You tell it "I want these numbers encoded with these types of bytes (i.e. char, uint32, float, double, etc.), and it spits out a string whos characters (which themselves are just bytes) represent those numbers. Then you can turn that string-of-bytes back into numbers again on the other side.

1 Like

Played around and it seems like floats are the best option for any Roblox replicating: only 4 characters, accurate enough for replication purposes, and I imagine its faster than any compression algorithm Lua can make due to being in C++.

Some small notes:

  • numbers in lua are usually doubles (8 chars), but some members of roblox objects (vector3 components for example) are indeed floats or ints
  • for some things that you don’t need that much precision for, you can probably get away with storing it in individual bytes (for instance, do you need more than 256 values for the direction a player is facing for rendering purposes?)
  • for integers, use a integer type. The uint32 type (or "I4" in string.pack terms) gives you exact representation of integers up to 4,294,967,295, while the float type stops being exact for integers at 16,777,217
1 Like

Next I need to test out how much data I can transfer without any performance issues, assuming that each character is 1 byte and that 30 players are present.
How should I measure performance?

Seems like the Stats service could help. Or, you could open the performance widget with ctrl+f7.

Yes but simply testing it would be difficult. You can’t imitate 30 players on a single client due to remote client limits. You also cannot simulate 30 clients in Studio for this because the server is hosted locally and it isn’t a fair test.

Oh gotcha. You could try to use something like clumsy to make localhost slower, not sure if that’s what you’re looking for though. It’s a good question though. I’m not sure how you’d go about getting stress test numbers for something like this.

1 Like

This isn’t enough info but I think that 25% of this should be a safe maximum to aim for.

Well, if you’re not sending physics data, and you’re smart about how often you send data, I suppose that’s doable. As long as that limit’s per-player, anyways.

1 Like

It is indeed per client. Assuming that you want to stay within 50% of the soft cap and you’re running at 60 FPS, you’re limited to 416 characters per replicated frame. Clearly, JSON is not allowed here. A lot of compression needs to be done to efficiently pack all the data into here.

2 Likes

For some reason, in practice, it took about 1000 characters per frame in order to get to 25kb/s. Not sure why.

Just a correction, I believe this measurement is actually 50 kilobits per second not kilobytes (outstanding) at least from testing this is where I seemed to get into throttling… That’s a whopping 8 times slower than I originally thought. At most you can fit about 106 bytes per network frame if this is in fact correct.

Generally, I have now taken into consideration number of requests more so than this theoretical limit because as far as I can tell Roblox will not actually spread out a request over more than one network frame even if its far past the limit. I think throttling comes down to future requests at that point? This leads to more CPU usage on the client for decoding, but, at this point I’m beginning to believe knowing there is a 50kbps limit is no longer helpful.

Generally, you’ll tend to see better network performance under the following conditions (from my testing at least):

  1. Strings. Strings sort of seem to bypass most of the CPU required for decoding. This makes them ideal if you want to introduce your own remote encoding as well. This is something I wasn’t aware of until recently.
  2. Minimizing the number of remote requests.

A little piece of useful info that might come in handy: RemoteFunctions will take approximately two network frames to complete a request no matter what. If you send a remote request on the server and have the client simply use an empty callback, basically, it will take one network frame to send the call to the client. It will then take an additional network frame (on the client) for the client to indicate a return.

Then this makes my test have even stranger results. Why would sending a 1000 character string 60 times a second to the server result in only 25kb/s traffic? I ran the test in studio and in-game, same results.

I have no clue, that’s a big reason why I’ve given up on that area. It seems that no matter how much testing I would do I would always end up finding inconsistencies under different circumstances and it honestly makes no sense to me. There are plenty of things you think would be a good measurement, and, they look to match each other, but then you start testing more things and you get wildly different results from your different methods of measurement.

If I had to guess, the netcode is very complicated, case-specific, and changes constantly, which is probably why roblox doesn’t document it anywhere. Wish they would, but what can ya do.