Search Unity

Question Synchronize IBufferElementData or any other array of data

Discussion in 'NetCode for ECS' started by YuriyVotintsev, Nov 29, 2022.

  1. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    I have an array of data that i need to synchronize between clients. This is an array of float3. I'm storing them using IBufferElementData. How can I achieve this with current version of netocde? (I can't predict this values and they change every frame/tick).
     
  2. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315
    Hey YuriyVotintsev!

    Simply add the [GhostField] attribute to the field(s) of the IBufferElementData, and replication will be handled (assuming it's a ghost etc).

    Ensure you have the ghost type set to Interpolated, high importance, and static optimized.

    However, we don't recommend sending data over 1kb in this fashion. It's worth seeing how well it compresses (via the NetDbg tool) to ensure you're not filling up (or fragmenting) your snapshots with this data.

    If possible, prefer to send large data outside NetCode (i.e. traditional socket TCP communication), prebake it, or calculate it deterministically via fixed point math and a seed.

    Cheers!
     
    Kmsxkuse likes this.
  3. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    Thank you, but why you don't recomend to send more than 1kb?
     
  4. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315
    GhostField data is stored in unreliable snapshots. At around 1kb of (assumed entirely uncompressed) data, you'll start to fragment these unreliable snapshots. This means that each client needs to receive 2+ unreliable UDP packets just to receive that 1 piece of data. I.e. If a client drops EITHER of those packets, they'll drop the entire snapshot.

    Thus, the problem gets exponentially worse as the packet size increases.

    RPCs are also not entirely suitable (even though RPCs are sent reliably), as fragmentation again triggers multiple packets, and multiple packets being sent to every client can trigger the reliable pipeline send queue (in the Unity Transport Package) to fill up, and throw.

    Thus: NetCode (and the Transport it's built on) are simply not designed for "large data". It's significantly better to set that data as a compressed blob via your matchmaking/matchfinding TCP / WebSocket service.

    If you absolutely must send a lot of data via netcode, try to batch that large data into small chunks, then filter those chunks based on Relevancy (and importance), and replicate those chunks as GhostFields.

    Out of curiosity: Would you mind replying with more details about the size of your data, and a screenshot of the NetDbg screen showing the per-client overhead with this approach? Feel free to DM if there is sensitive data (or not send at all, of course).
     
    Opeth001 likes this.
  5. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    Thank you for the detailed answer. I am syncing full pose of vr character calculated by VRIK. There are 26 bones with rotations and positions. I am targeting 10 to 30 players in one game. I can't calculate VRIK on client, because client is on mobile processor (quest2) and vrik is singlethreaded and creates high cpu load. So i calculate vrik on server with good cpu and send poses to clients. NetDbg screenshots will send after tests.
     
  6. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315
    Ah fair. Two more nuances, done with my own testing:
    • The way Snapshots work is that they'll resend a chunk of ghosts (to a specific client) until that client acks a snapshot with those ghosts in them. Thus, you will almost certainly send the same data multiple times, to every client, before each of them replies with an ack. The quantity of resends depends on Importance, Relevancy, GhostCount, and Ping. This is true even with static optimize enabled.

      In my own test, the server sent the 1kb of data (compressed into 400bytes) IBufferElementData to the client 14 times before the client acked it.

    • The IBufferElementData serializers do delta-compress (via Huffman encoding) every 4byte stride, so naïve compression could be okay. As mentioned, in my test it was about 40% of the original size.

      Thus, I also highly recommend you aggressively quantize your float data to make this better.

      You may be able to use some assumptions about VRIK data to improve this, but it's tricky.

      RPCs may therefore be more appropriate (although you cannot use the NetDbg tool to visualize RPC overhead).
     
    Last edited: Nov 29, 2022
  7. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    I implemented this approach, but i don't understand why my characters are shaking. One frame charcters move two frame forward, next step he returns back one frame, then again two frames forward, then again one frame back. On client i just use data in buffer to place skinnedmeshrenderer bones to data in buffer every presentation system group update. Everything works ok, except for this shaking.
     
  8. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    I don't know the details, but if you are using the buffer to store the current state of the bones (for a given tick) and the bones transforms are calculated/update as part of the prediction loop, then things should work.

    the client is ahead of the server, so lets say 2/3 tick (server frame 10, client frame 13).
    When the client received data from frame 11 (rollback 2 tick) it will start re-predicting up to the next tick, tick 14.

    When he receive the data from the server, the state of the buffer is restore to the tick 11. Unless there is a system that calculate the bones position inside the GhostPredictionGroup, you will constantly see this back and forward.
     
  9. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    I don't use prediction at all. I can't predict position of this bones. I want to use only server data without any predictions or interpolations. Why it jumps back and forth if i don't use prediction?

    Some details:
    Code (CSharp):
    1. [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
    2. public partial class PhantomPositionSystem : SystemBase
    In this system, on Server, i calculate position of my character (phantom) and store all data in

    Code (CSharp):
    1. [GhostComponent]
    2. public struct PhantomBone : IBufferElementData
    3. {
    4.     [GhostField] public float3 WorldPosition;
    5.     [GhostField] public quaternion WorldRotation;
    6. }
    After that, on Client, i just copy data from buffer to bones transforms:
    Code (CSharp):
    1.  
    2.         public override void DotsUpdate()
    3.         {
    4.             if (!World.EntityManager.Exists(Entity))
    5.                 return;
    6.  
    7.             var bones = World.EntityManager.GetBuffer<PhantomBone>(Entity);
    8.  
    9.             for (int i = 0; i < bones.Length; i++)
    10.             {
    11.                 _vrikSourceInstance.Bones[i].position = bones[i].WorldPosition;
    12.                 _vrikSourceInstance.Bones[i].rotation = bones[i].WorldRotation;
    13.             }
    14.         }
    DotsUpdate is method that i launch in PresentationSystemGroup:
    Code (CSharp):
    1.     [UpdateInGroup(typeof(PresentationSystemGroup))]
    2.     public partial class ViewSystem : SystemBase
    3.     {
    4.         protected override void OnUpdate()
    5.         {
    6.             foreach (var view in SystemAPI.Query<Components.View>())
    7.             {
    8.                 view.ViewBehaviour.DotsUpdate();
    9.             }
    10.         }
    11.     }
     
    Last edited: Nov 30, 2022
  10. Occuros

    Occuros

    Joined:
    Sep 4, 2018
    Posts:
    300
    Hey @YuriyVotintsev, are you sure that your bottleneck will be VRIK?

    If you try to sync over 10 players, you will most likely be GPU bound if these players have full-body avatars. You will most likely need a LOD system where most other users won't have full IK if they are not close by (unless you are working on a meeting app where the majority of interactions is just the avatars moving and no gameplay).

    I wonder if you are trying to solve a bottleneck that might not really exist (with the IK being calculated on each client). From our tests, the IK calculations on avatars are usually not the bottleneck (but rather the other parts of gameplay).

    Have you already tested that the IK is the primary issue? Also, have you implemented your IK calculations fully in burst?
     
  11. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    We have other similar game fully featured. And VRIK takes nearly 30% of cpu time. If this approach will not work, we will try to optimize vrik with other options.
     
  12. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    How did you configure the character ghost??

    I suppose that, because it is the player character I presume (just presume) you setup it as OwnerPredicted. In such a case, the ghost is predicted and that is pretty obvious why the buffer data go back in time.

    Are you seeing the character position moving back and forth or are the bones that get completely wrong?
    Also, because the character is moving again forward, what system is responsible to move the character? Who is handling the input (if any)?

    If the entity is interpolated, the character bones you are using should be the one at InterpolatiomTick (so old of about at least 2 ticks plus some extra to compensate for jitter if present).
    The InterpolationTick never goes back in time (in normal circumstances) so the behaviour would be odd.
     
  13. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    Oh. It was my fault. There was nothing to do with netcode about this shaking. It was because i use world positions in syncing and "random" order of applying this positions back to character. Now i use local positions and everything works as expected.

    The only problem left - is really big data. There is screenshot of netdbg and gif how it look in editor:
    Screenshot 2022-11-30 194910.jpg
    Screenshot 2022-11-30 194935.jpg
    vrik32.gif

    I didn't try it in build, because there are some strange runtime exceptions in build (prefabs missing or something like this).
     
  14. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    oops, forgot to enable quantization:
    client.jpg
    server.jpg

    looks much better (~8 times per entity)
     
  15. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315
    It looks like the server is trying to send too many ghosts in a single packet.
    Now would be a very good time to add "Distance Based Importance Scaling".

    Which would work beautifully with tweaks to the `GhostSendSystemData`. See the 0.8.0 changelog entry: Added parameters to control how much data the server serializes based on CPU time in addition to bandwidth. The parameters are MinSendImportance, MinDistanceScaledSendImportance, MaxSendChunks and MaxSendEntities.

    So, I'd try some combination of things (assuming the base "Importance" of the Phantom ghost is 1):
    • `MaxSendEntities` to 20-100, reducing the issue you have now.
    • `MinSendImportance` to ~10 (so a ghost is only replicated in every tenth snapshot, by default).
    • Edit2: `NetworkStreamSnapshotTargetSize` should be used to force a desired snapshot size, which will help enforce a specific KBPS target for server send (egress), and client ingress (receive).
     
    Last edited: Dec 6, 2022
  16. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315
    Just adding: My earlier comment about `NetworkStreamSnapshotTargetSize` was wrong (now corrected). You can add `NetworkStreamSnapshotTargetSize` to each connection entity (on the server) to force that connections snapshot to be a given size.

    Example: 416 bytes * 60hz = 200kbit/s. For reference, most AAA shooters these days tend to be around 50 to 200kbit/s.

    Important context is that this 416 bytes value:
    • Does not affect RPCs, commands, or control messages.
    • Does include UTP header overhead, nor UDP header overhead.
    • I.e. It's only payload bytes.
     
  17. YuriyVotintsev

    YuriyVotintsev

    Joined:
    Jun 11, 2013
    Posts:
    93
    I am trying to implement now this distance-based importance stuff. But i really don't understand how it works. I did everything like in asteroids sample, the only change is reducing tile size to 3m, but every my entity updates with equal interval, i don't see any effect from distance-based importance. How to make it work so that ghosts in 5m radius updates every tick and ghosts on 25m radius updates every second?
     
  18. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    315