Synchronising movement animations in networked games

Started by
8 comments, last by itay9 3 years, 8 months ago

Hello guys,

Recently I've modified my PlayerController script to accept the following kind of strafe-s:

Front, Front-Right, Front-Left, Back, Back-Right, Back-Left, Right, Left.

instead of the previous animation I've had which is only front.

Meaning now players are able strafe in different manners without rotating their character (similar to many action titles).

Now I'm wondering which way would be best to synchronise the correct type of movement a player performed on a remote client:

For instance- in my local machine I might have strafed Front-Right, however without the original X,Y axes inputs the remote client will have no idea that the player actually strafed, and if it did, it wouldn't be able to reverse the vector3 delta to the axes inputs.

In such a case I've considered the following option:

Construct my movement packets as follows:

MoveType MoveType; (All kinds of strafes + Idle + Animation)
Float Speed;
Quaternion Rotation

Using these inputs I can build the position delta that the player traveled, though it would also incur the following change to the `AgentState` which is the data that is sent to other clients every tick X times:

Vector3 position; (server position)
Quaternion Rotation; (server rotation)
MoveType MoveType; // newly added for calculating the animation

I think that the above modifications conflict with the separation between: Networking, Logic, View - since I have added another variable simply for synchronising an animation.

Do you guys have a better suggestion that this?

Advertisement

Generally, you will need to synchronize four quantities for a physical entity: Position, Velocity, Orientation, and Spin.

For inputs commands, your movement input typically has either nine discrete states (the eight cardinal directions, and “still”) or it has two continuous values (front/back and left/right.) Additionally, you will have two separate axes for aiming: left/right and up/down. On top of that, you may have movement modes, that may come from control input, or may come from physics – walk/run/crouch/jump versus fall/swim, for example.

Exactly how you decide to synchronize these from client→server, and how you then decide to forward them on from server to viewers, depends entirely on your specific game design and goals.

Finally, whether you derive animation state on each viewer, based on physical motion/position, or if you declare animation presentation as a quantity you synchronize over networking, is entirely up to you. There's absolutely nothing wrong with putting presentation options in networking. Consider a car, that may have turn signals, and a horn. Turn signals or a horn don't affect how the car drives, only how it presents, but they should likely be part of the car state on a network, for other people to see them correctly!

enum Bool { True, False, FileNotFound };

@hplus0603

hplus0603 said:
Generally, you will need to synchronize four quantities for a physical entity: Position, Velocity, Orientation, and Spin.

Position and Orientation are the values that I used to sync (12 + 16 bytes), with velocity it's another 4 bytes, and spin (not sure about it's type I will assume it's a Quaternion - 16 bytes) in total it's 48 bytes, for a performant tick updates isn't it relatively a lot? If we take 1500 bytes (max udp packet size without fragmentation) and divide that packet size we get 31.25 states that we can send per packet.

So if I have 20 entities which are updated per tick it would mean I can send up to 1.5 states for each entity per packet, is that a good rate?

It seems to me that an Entity's state should be as small as possible, most info on networking over the web (at least what I've tackled), don't take those animation stuff into consideration.

First, a few assumptions that aren't necessarily true.

First, on the number of bits:

A FPS can often get away with using 24 bits fixed point for each of X and Y in position, and maybe as low as 20 bits for Z. This would, for example, give you +/- 8000 meters with millimeter precision for X/Y, and +/- 500 meters for elevation. For velocity, you can often get away with 16 bits for all quantities – this will give you +/- 32 meters per second with millimeter per second precision. (that's 100 kmph, which is fast enough for most FPS characters running.) You can cram this into 8 bytes if you really try, depending on size of level and precision needed.

For orientation, you often only need heading and pitch – most FPS don't actually require twist around the forward looking vector. You're always standing straight up, unless a special camera affecting animation is playing, and you don't sync the physical object to that orientation. For a non-character object, you need the three axes, but you don't need to send a full quaternion for this; you can make sure the largest value is positive, drop the largest of the four values, and send two bits for which axis was dropped, and then send the other three axes. I know of games that send a quaternion using 32 bits total; 2 bits for the eliminated axis, and 10 bits each for the other three axes. Note that when you drop the largest value, the next largest can be at most sqrt(0.5) in magnitude, so you don't even need to map the 10 bit range to the full -1 .. 1 range.

For spin, you will likely need all three axes. In most physics systems, spin is actually stored as the axis of rotation, with the length of the axis being the spin velocity (this ends up working out well when you multiply it out with the time delta and a quaternion to transform it.) So that's another three quantities, at whatever precision you prefer.

Depending on how precise and big you need this to be, you are looking at between 16 and 32 bytes for a full snapshot.

Second, on the size of a packet:

A single IP datagram is limited to 1280 bytes minus packet overhead, to make sure you don't get fragmentation on an IPv6 link or an Ethernet link. 1500 bytes is the size without fragmenting on Ethernet without jumbo frames, but half of all players don't even use Ethernet anymore, they use WiFi or gig ethernet with jumbo frames. And Ethernet fragmentation isn't actually a big deal. You can jam a UDP packet full with 64 kB of data, and send it to the other end of the earth, and with very high likelihood, the full packet will make it to the other end. RakNet does this, and goes one step further – it may send multiple, big, UDP datagrams, and pack self-correcting codes into the different packets, so that if one is lost, the full datagram can be reconstructed anyway.

Sure, sending a megabyte per second for an 8-player FPS will not be viewed kindly by your players, but 30 bytes per player, 50 times a second, for 20 players won't break the bank for most players on the current internet. If you're a very small studio, it's probably not the most important quantity to optimize further. (30*20 is just 600 bytes, btw, sending that 50 times per second is 30 kB/second.)

enum Bool { True, False, FileNotFound };

@hplus0603

hplus0603 said:
A FPS can often get away with using 24 bits fixed point for each of X and Y in position, and maybe as low as 20 bits for Z. This would, for example, give you +/- 8000 meters with millimeter precision for X/Y, and +/- 500 meters for elevation. For velocity, you can often get away with 16 bits for all quantities – this will give you +/- 32 meters per second with millimeter per second precision. (that's 100 kmph, which is fast enough for most FPS characters running.) You can cram this into 8 bytes if you really try, depending on size of level and precision needed.

I'm using c# as my language of choice so I have a hard time compressing values into bits, I thought of cramming float values by multiplying by 256 before sending and dividing by 256 upon arrival, though the floating point turns up like this:

`52.1 becomes 52.09765625.`

Are you familiar with any solutions for c#?

About syncing the spin, if I sync the rotation (a Quaternion) the entity is looking towards the correct location, in that case why would I want to sync the spin?

Do I over-exaggerate the optimization of EntityState packet?

Lastly:

Sending the `MoveType` as an extra byte (probably can be reduced to 2bits) is not a wrong choice, and there is nothing wrong with syncing extra quantities that are relevant for the game?

I have a hard time compressing values into bits

Why? C# has byte arrays. You can marshal by making up an integer and splitting into bytes.

Something like:

            float theFloat; // whatever your value is
            if (theFloat < -8191) { theFloat = -8191; }
            if (theFloat > 8191) { theFloat = 8191; };
            byte[] b = new byte[3];
            uint i = (theFloat + 8192) * 1024;
            b[0] = (byte)((i >> 16) & 0xff);
            b[1] = (byte)((i >> 8) & 0xff);
            b[2] = (byte)(i & 0xff);
            // now, send these three bytes
            
            // to unpack
            float unpackedFloat = (((uint)b[0] << 16) | ((uint)b[1] << 8) | ((uint)b[2])) / 1024.0f - 8192.0f;

`52.1 becomes 52.09765625.`

Yes, that's what truncating resolution means. (FWIW, 52.1 isn't actually perfectly representable even using a full floating point value, because it's not a fractional power of two.)

“spin” is angular velocity, “orientation” is angular position, just like you have speed and position. You need to sync spin for any physical entity that is simulated (weapons, barrels, blocks, rocks, etc) but if your character is kinematic ("irresistible force, immovable object,") you obviously don't need to do that.

enum Bool { True, False, FileNotFound };

@hplus0603

Thanks for the example!

hplus0603 said:
Yes, that's what truncating resolution means. (FWIW, 52.1 isn't actually perfectly representable even using a full floating point value, because it's not a fractional power of two.)

I guess it's okay to lose a slight precision for a far greater performance.

hplus0603 said:
“spin” is angular velocity, “orientation” is angular position, just like you have speed and position. You need to sync spin for any physical entity that is simulated (weapons, barrels, blocks, rocks, etc) but if your character is kinematic (

That makes sense now, the spin is sent as a measure to perform physics prediction I assume? (I didn't deal with ragdolls yet).

I'm now wondering whether I should keep optimizing my net code, or work on the gameplay (which is the selling point of the game).

I'm in the mid-late PoC stage, would you suggest leaving net-code details for the Production phase, whilst moving on to develop the game's mechanics and features?

On the one hand, the game MUST be built with networking in mind from the beginning, or you will have a hell of a time trying to bolt it on later (that never works out well.)

On the other hand, you don't need to tune bandwidth consumption or special-case bugs for a proof of concept. As long as players can play and have fun together, that's good enough for now, because with networking being “always in the loop” during development, you're in a much better place overall.

And, yes, making “a fun game that people want to play” is the hardest challenge in gamedev, so should receive the most attention if you want to succeed :-)

enum Bool { True, False, FileNotFound };

@hplus0603

Thanks for your kind knowledgeable and kind response. ?

This topic is closed to new replies.

Advertisement