Modify NGO NetworkTransform for Shifting World Origin

I have a system to shift the world back to the origin if the player passes a threshold. All Objects in the world have a 64BIT position. Now each client has its own world offset because they could be in completely different areas of the game. So each client has a 64bit world offset which you would need to add the all the transform.position to get the real position.

I now sync these world offsets and can translate between the different clients positions. But I would still like to use the NetworkTransform to sync positions, because it obviously is well maintained with minimal bugs and good performance.

So I plan on sending the shifted position from the authorative client and then on the client that receive this state transfrom it to the realPosition and back to their shiftedPosition. With this approach I should be able to use the NetworkTransform and not send 64BIT position over the network and only update them if a client shifts their world.

But I looked into it and I cant seem to find any good points where I can intercept the position that each client receives from the authoritative client and transform it to their offset. No virtual methods give me an interception point.

I think the places that I need to modify are the “ApplyTeleportingState” and “ApplyUpdatedState” methods, where changing “ApplyUpdatedState” would probably be enough because the other method is called form there accept for one other place that I dont think matters.

So as far as I can tell I should probably modify the input paramters “NetworkTransformState” of “ApplyUpdatedState” and modify the position of the struct. But its all private.

  1. So the solutions to this problem that I currently see its either do something weird with reflections and intercept some private field or something. Which I really dont like
  2. Copy the entire NGO Package into my project so that I can modify it / access internal classes, so that I can actually modify the NetworkTransform class. Which I dont like.
  3. Implement my own Solution for a “NetworkTransform”, which I feel like is a really bad idea and a giant waste of time.
  4. I have missed something and this can be done easily. Because one of the key points of NGO was that it can be used for a variety of use cases and can be tuned to ones needs. (I really hope this is the case)

I strongly urge you to reconsider! :wink:

You’re right about the first two parts, but performance is terrible when you are looking at what you can do if you send a single message for many game objects as a byte array, and then also pack the data using the BytePack utilities and on top also brotli compress the byte array before sending if you can afford a bit of frame time dedicated to reducing traffic.

In my test with 100 constantly moving game objects the naive approach with NetworkTransform, although optimized to only send half float and delta updates, used up five times more traffic than a single message with a byte array using the pack utilities (ie vector3 => half3 and similar optimizations where possible). Adding Brotli compression on top reduced the byte array by half to a quarter of the size.

A system like yours demands that you take control over your data to the fullest!

It’s neither and relatively easy. After all, in its purest essence it just requires hooking into the network tick update and then packing whatever you need of position, rotation and scale, and then call an RPC. But ideally you want to use custom (named) messages, and make that update once for all (possibly “registered”) game objects in one sweep.

As you figured, NetworkTransform doesn’t provide you with good hooks. Personally I’ve come to conclude that these Network* behaviour components exist purely to make it dead easy for the beginners and intermediates but they’re ultra-wasteful if your game needs to scale, or they’re just not supporting your game’s needs.

Another case in point: NetworkAnimator. Just read the docs, it’ll tell you it synchs even transitions. Now imagine a typical FPS: all of the data for animations is probably already available ie velocities so the animators can pretty much run locally. On top, at best you need to send a bool like “fire” or “jump” to move the client’s animators in the right state. So that’s minimal traffic compared to continuous per-tick synchronization of the animator’s state.

Also added bonus for going the custom route: you no longer need to deal with the various quirks of NGO, such as around parenting. You can stuff your game objects into categories like in singleplayer, rather than having them all rooted in the scene.

1 Like

Thanks a lot. Very interesting. I just assumed it would have strong optimization looking at how much code they have written for their components. I mean the C# performance never seemed to be good at all, but I thought at least the networking performance would have to be good then. I just couldn’t imagine that they produce something this bad, at Unity.

…

Creating a Custom implementation was really fast and easy using a LLM.

Yeah but they have to cover ALL imaginable use cases and platforms.

Case in point: I wanted to use the PlayerInputManager + PlayerInput in splitscreen network. Both classes combined are over 2k lines of code. Ultimately what I really needed after stripping it all down was just 200 lines because I only support mouse, keyboard and gamepad, none of the touch input oddities and other edge cases.

This isn’t necessarily bad, Unity just has to cover all those edge cases, no way around that. Whereas we are targeting only a subset of platforms and using only a subset of features, so sometimes it’s best to consider Unity’s code as templates to modify if and only if you can afford spending that time, either because you have to or because it will be worthwhile.

Yeah for sure. But good code should be able to support all kind of things while maintaining at least similar performance. Sure its not always easy to design your code in a way where you have 0 cost abstractions, but this clearly is very very far from what a 0 cost abstraction is, as you described it. I guess they really didn’t but any focus on performance. But well maybe one should use DOTS if performance is that important, probably they have a way bigger focus on performance in Netcode for Entities. But that a whole different topic.

Think I am about done with the implementation and as you said it is way less traffic. The test uses 100 GameObj, 30ticks with random perlin noise movement and a Network Simulator on the “Mobile 4G” preset. NGO NetworksTransform uses about 60kb and my implementation takes 18kb looking at the wireshark udp packets.

I set Unitys Network Transform to some pretty heavy optimizations with unreliable deltas, half precision and only syncing position and y rotation, 0.1 position threshold and 1 degree rotation threshold.
My implementation does have some pretty heavy optimization with just sending the exact bits that are required (this does not save all that much) and just naive interpolation without time stamps.

But I really think you get most of what I have when you just use halfs instead of floats, dont sync a ulong to identify the NetworkObject but use a ushort, because youll never have more NetworkTransforms then that anyway. Just Sending the rotation axis you need. Only change updates when the player moved are rotated by a significant amount and packing all Updates into a single message/RPC. The Brotli compression doesnt even do all that much for me, its about 10-15% of traffic saved with Optimal compression, but its nice to have.

I do not use any delta synchronization, but due to my special setup, where I reshifts my world to the origin I can ensure that every point is already pretty small and use 16bit at max, depending on the precision I set.

So I can confirm that Unity NetworkTransform is pretty slow. I am not really sure why it is so slow. There is no reasonable explanation why their their implementation takes atleast 3x more bandwidth. I mean a little more can be expected, maybe 2x, but 3x is just over the top. Well I guess it is just bad.

1 Like

That surprised me too. Glad you could confirm it. Even though I was sure I had covered all possibilities of me interpreting the values incorrectly or giving my solution an unfair advantage over NetworkTransform, it still left me wondering.