4

I'm currently building my own netcode for a Unity game (for learning experience) and I'm running into some serious delay on the "decoding packets" side of things.

Basically, I have playerObject's that send their position data (Vector3(x,y,z)) as a JSON string to the server, and the server sends that back out to all other players.

The socket side of things are moving along nicely. Very little delay from when a packet is created to when a packet is received.

But I am getting a massive delay on my Clients when they are trying to un-JSON the remote client's position:

Vector3 remotePos = JsonUtility.FromJson<Vector3>(_command.Substring(5 + _usernameLength));

the Json string has an identifier at beginning of the string to say that it is a Position update, followed by two numbers signifying the length of the username, then the username (so that the remote clients can update the proper playerObject. The whole string will look something like this.

POS09NewPlayer{"x":140.47999572753907,"y":0.25,"z":140.7100067138672}

After receiving such a packet from the server, my clients will preform the following task:

        int _usernameLength = Int32.Parse(_command.Substring(3, 2));
        string _username = _command.Substring(5, _usernameLength);
        Vector3 remotePos = JsonUtility.FromJson<Vector3>
        if (_username != username)
        {
        playerDict[_username].transform.position = remotePos;
        } 

All of this "works", but gets very sluggish after just 3 clients connect simultaneously.

What am I doing wrong? There must be a significant flaw as I am only sending updates every .015 seconds for 3 players, where as games like Battlefield can send 60 updates a second for 64 players!

Any advice would be appreciated.

MrDysprosium
  • 489
  • 9
  • 20
  • 1
    at least move `JsonUtility.FromJson` in the `if` – hyankov Mar 05 '17 at 16:35
  • I highly doubt json deserialization can cause such huge impact in this case – Evk Mar 05 '17 at 16:43
  • @Evk I'm counting received packets while counting json'd packets. I am receiving packets about three times as quick as I'm able to json them. Which means stuff falls way behind. – MrDysprosium Mar 05 '17 at 20:49

1 Answers1

18

Well, there is certainly room for optimization. Since you are quoting big game engines as a reference, I will give you some examples based on the older Unreal Engine versions and some of the optimizations they are using:

  • Network packages use the UDP protocol. It still needs to take care of dropped, duplicated or out-of-order packages on its own, but it swallows that bitter pill instead of using an actual TCP connection because the latter would introduce even more overhead.
  • The server keeps track of which information updates it did send to clients. For every object on the client side that is replicated by the server, the server keeps track of all variables. At the end of each game loop update, the server will check which variables actually have different values from the last time the server sent an update to the client. And only if there is actually a difference, the new value is sent to the client. That may seem like an incredible overhead, but network bandwidth is a much scarcer resource than system memory and CPU time.
  • Vector information is actually truncated before being sent over the network. The engine uses float as datatype for its vector components, but it still rounds X, Y and Z to the nearest integer and transmits those instead of the full floating point data. That way they only take up a couple of bits to a few bytes, rather than 12 bytes of the uncompressed original floats.
  • Position updates are simulated on the client-side and merged with the incoming data from the server. To avoid jittering from position updates, clients predict the new position of an object from its velocity value. When the client receives a position update from the server later on, it will gracefully move the object slightly to the new position (basically a compromise between the client and server coordinates) and only do a hard update if the server's position differs too much from the client's version. This is done with a lot of things. If a player shoots a projectile, that shot is performed on the server, which will create the projectile and send it to the player to update its position. But the player will also receive the function call about the shot and create the projectile locally and update its trajectory. Even if that projectile can't actually kill a player on the client side, its trajectory and effects can still be simulated locally to give the impression of smooth gameplay. If it hits something, the appropriate effects can be played at the location without requiring the server to tell the client to create those effects.
  • The server keeps track of what is relevant to each player. If a player is on the other side of the map behind several walls, it doesn't need to send you any updates about that players actions. And if a projectile of that player happens to come into your view later on, the server can still tell you about those projectiles at the appropriate time.

The whole matter is a lot more complex than what little I can cram into this post, but the gist of it is probably: network bandwidth is the most limited resource in multiplayer games, so the engine goes out of its way to send as little and few information packages as it can get away with.

Notice something? Less than a single component of your position vector would already be considered way too large for the entire vector in this engine. I guess JSON really introduces some overhead here, simply due to its verbosity. You are sending numbers as strings when you could also be just sending their actual bits and infer their meaning from the actual package.

Crusha K. Rool
  • 1,502
  • 15
  • 24
  • 2
    Wow, thank you for that write up... lot's of useful info. Could you elaborate a bit on that last part? "you could also be just sending their actual bits and infer their meaning from the actual package." – MrDysprosium Mar 05 '17 at 17:50
  • It was more a hint at how actual engines are probably doing it, since they likely aren't using JSON. If you work with something as barebones as UDP, you are free to send whatever bytes you want to (up to a maximum of roughly 65000 bytes per package). You could then have a few bytes to identify the player, some to identify the data being sent and then just a few for the actual numeric values of the vector and parse it yourself on the client. An example for UDP in .NET: https://social.msdn.microsoft.com/Forums/en-US/92846ccb-fad3-469a-baf7-bb153ce2d82b/simple-udp-example-code?forum=netfxnetcom – Crusha K. Rool Mar 05 '17 at 18:01
  • 1
    Keep in mind though that UDP doesn't take care of duplicated, dropped or out-of-order packages on its own, so you may need to handle that on your own. That means sending some form of acknowledgement that the package was received, and otherwise have the sender re-send a package in regular intervals until an acknowledgement is received. And timestamping outgoing packages, so old data can be dropped on the receiving side if newer data arrived beforehand (but still sending the acknowledgement, so that the sender doesn't keep sending these packages). – Crusha K. Rool Mar 05 '17 at 18:05