Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Question Movement jittery between client and server values

Discussion in 'NetCode for ECS' started by Jehy, Dec 7, 2023.

  1. Jehy

    Jehy

    Joined:
    Sep 25, 2014
    Posts:
    8
    I'm trying to debug movement issues with my Netcode for Entities project.
    Hopefully someone here can help me :)

    Here is a video showing the issue:


    My player rapidly flickers between the Client (predicted) and Server positions.

    The Server position is always behind the Client (predicted) position, seems normal.
    But (I think) my cube is also flickering between rendering at the server position and the client position.

    The project is expanded from the Networked Cube example.

    This is the inspector on the player prefab:
    upload_2023-12-7_21-26-45.png

    I am trying to move the player with PhysicsVelocity instead of directly modifying the LocalTransform.

    This is the system that applies movement to the cube.

    Code (CSharp):
    1. [BurstCompile]
    2. [UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup))]
    3. public partial struct CubeMovementSystem : ISystem {
    4.     ComponentLookup<Ability> ability_lookup;
    5.  
    6.     [BurstCompile]
    7.     public void OnCreate(ref SystemState state) {
    8.         ability_lookup = SystemAPI.GetComponentLookup<Ability>(true);
    9.     }
    10.  
    11.     [BurstCompile]
    12.     public void OnUpdate(ref SystemState state) {
    13.         foreach (
    14.             var (
    15.                 transform,
    16.                 phys_velocity,
    17.                 input,
    18.                 movement,
    19.                 abilities
    20.             ) in SystemAPI.Query<
    21.                 RefRW<LocalTransform>,
    22.                 RefRW<PhysicsVelocity>,
    23.                 RefRW<NetPlayerInput>,
    24.                 RefRW<Movement>,
    25.                 DynamicBuffer<Abilities>
    26.             >().WithAll<GhostOwnerIsLocal>()
    27.         ) {
    28.             ability_lookup.Update(ref state);  // Update the state of ComponentLookups
    29.  
    30.             float speed = movement.ValueRO.speed;
    31.             float speed_max = movement.ValueRO.speed_max;
    32.  
    33.             // Code related to dash ability removed for the sake of readability
    34.             // All logic is conditional on input and bug happens without using dash
    35.  
    36.             // Enforce physics constraints
    37.             ///////////////////////////////////////////////////////////////////
    38.             phys_velocity.ValueRW.Linear.y = 0;
    39.             transform.ValueRW.Rotation = quaternion.Euler(
    40.                 new float3(0, transform.ValueRO.Rotation.value.y, 0)
    41.             );
    42.             if (math.length(phys_velocity.ValueRO.Linear) <= math.EPSILON) {
    43.                 phys_velocity.ValueRW.Linear = float3.zero;
    44.             }
    45.  
    46.  
    47.             // Player input movement
    48.             ///////////////////////////////////////////////////////////////////
    49.             bool has_player_movement = input.ValueRO.move.Equals(float2.zero);
    50.             if (!!!has_player_movement) {
    51.                 movement.ValueRW.Velocity.Linear.x += input.ValueRO.move.x * (speed * SystemAPI.Time.DeltaTime);
    52.                 movement.ValueRW.Velocity.Linear.z += input.ValueRO.move.y * (speed * SystemAPI.Time.DeltaTime);
    53.             }
    54.  
    55.  
    56.             // Enforce movement limits
    57.             ///////////////////////////////////////////////////////////////////
    58.             if (math.length(movement.ValueRW.Velocity.Linear) > speed_max) {
    59.                 movement.ValueRW.Velocity.Linear = math.normalizesafe(movement.ValueRO.Velocity.Linear) * speed_max;
    60.             }
    61.  
    62.  
    63.             // Apply movement to physics
    64.             ///////////////////////////////////////////////////////////////////
    65.             if (math.length(movement.ValueRW.Velocity.Linear) > math.EPSILON) {
    66.                 phys_velocity.ValueRW.Linear.x += movement.ValueRO.Velocity.Linear.x;
    67.                 phys_velocity.ValueRW.Linear.z += movement.ValueRO.Velocity.Linear.z;
    68.             }
    69.         }
    70.     }
    71. }
    72.  
    This is the component that holds the player input:
    Code (CSharp):
    1.  
    2. [GhostComponent(PrefabType=GhostPrefabType.AllPredicted)]
    3. public struct NetPlayerInput : IInputComponentData {
    4.     public float2 move;
    5.     public InputEvent dash;
    6. }
    Component holding player-driven movement:
    Code (CSharp):
    1.  
    2. [GhostComponent(PrefabType=GhostPrefabType.AllPredicted)]
    3. public struct Movement : IComponentData {
    4.     public PhysicsVelocity Velocity;
    5.     public float speed;
    6.     public float speed_base;  // Allow modifying speed
    7.     public float speed_max;
    8.     public float speed_max_base;  // Allow modifying max speed
    9. }
    Camera follow system:
    Code (CSharp):
    1. public partial class CameraSystem : SystemBase {
    2.     protected override void OnUpdate() {
    3.         float3 world_position = float3.zero;
    4.  
    5.         foreach (var (
    6.             _,
    7.             player,
    8.             local_transform,
    9.             local_to_world,
    10.             entity
    11.         ) in SystemAPI.Query<
    12.             GhostOwnerIsLocal,
    13.             RefRO<Player>,
    14.             RefRO<LocalTransform>,
    15.             RefRO<LocalToWorld>
    16.         >().WithEntityAccess()) {
    17.             // Debug.Log($"Local Position: {local_transform.ValueRO.Position}");
    18.             world_position = local_transform.ValueRO.Position;
    19.         }
    20.  
    21.         foreach (var (
    22.             camera,
    23.             camera_settings,
    24.             local_to_world
    25.         ) in SystemAPI.Query<
    26.             MainCamera,
    27.             RefRW<MainCameraSettings>,
    28.             RefRW<LocalToWorld>
    29.         >()) {
    30.             camera.look_at = world_position;
    31.             camera.Transform.LookAt(world_position);
    32.             camera.Transform.position = SystemAPI.GetComponent<LocalToWorld>(camera_settings.ValueRO.Position).Position;
    33.             // Debug.Log($"World Position: {world_position}");
    34.         }
    35.     }
    36. }
    37.  
    Camera components:
    Code (CSharp):
    1.  
    2. public class MainCamera : IComponentData {
    3.     public Transform Transform;
    4.     public float3 look_at;
    5. }
    6.  
    7. public struct MainCameraSettings : IComponentData {
    8.     public Entity Pivot;
    9.     public Entity Position;
    10. }
    11.  
    The MainCameraSettings hold references to Entities that are initialized by a proxy component just like in the ECSSamples PhysicsSamples. They are Child GameObjects of the Player prefab with only a transform -- then baked.

    Package versions:
    Entities: 1.0.16
    Netcode for Entities: 1.0.17
    Unity Physics: 1.0.16
    Mathematics: 1.2.6

    EDIT: In this case, the results were caused by the camera follow and was completely unrelated to the movement. See posts below.
     
    Last edited: Dec 8, 2023
  2. SamuelBellomoUnity

    SamuelBellomoUnity

    Unity Technologies

    Joined:
    Sep 24, 2020
    Posts:
    8
    Without the full picture, here's some guesses and some steps to debug

    First thing I'd look into is making sure your system runs before the physics simulation (check out NetcodeSamples' physics sample)
    [UpdateBefore(typeof(PhysicsInitializeGroup))]

    From your video, playing frame by frame looks like there's a discrepancy between your camera, your cube and the client debug mesh. Your camera and cube seem super smooth which is suspicious. What's your setup for camera following vs your cube? Normally the debug mesh is supposed to draw every frame your ghost's LocalToWorld. Are there extra entities used for rendering?
    Quick sanity check: what does your movement component look like? Are its fields replicated with GhostField? (Any reason why you're not just using phys velocity's values there? why the extra velocity in your movement component?)
    This shouldn't cause your issue, but can you add a check for the Simulate tag please, that'll make sure you simulate only when needed.
    What package version are you using?

    In general, to debug this, I'd add Debug.Logs and Debug.DrawLines to see if 1. your OnUpdate is executed the expected amount of times when resimulating and 2. if there's anything blocking/preventing your entity's movement. Debug.Drawlines with a lifetime of one frame should show you frame per frame all the resimulation steps that happened in that frame. Then screen record your scene view and step frame by frame.
     
    Jehy likes this.
  3. Jehy

    Jehy

    Joined:
    Sep 25, 2014
    Posts:
    8
    Thanks for taking the time Samuel!
    • Added camera follow system
    • Added movement component
    • Not sure if/not replicated GhostFields
    • Added package versions
    I have a separate player movement so that I can add external forces and limit player-driven movement forces.
    I assume I'll need to know both the physics velocity and the player-movement velocity to do this.

    At the moment the code system just clamps all movement beyond the
    max_speed
    but that's just because I have been stripping things away and simplifying to try and figure out what's causing this issue.

    I'll add more Debugs to investigate and update here with results.
     
  4. Jehy

    Jehy

    Joined:
    Sep 25, 2014
    Posts:
    8
    Upon further investigation, the issue has to be with the camera following, not the movement.

    When disabling the camera-follow, the issue is gone.

    The best results so far come from running the CameraSystem in PredictedFixedStep, but after CubeMovementSystem with these attributes:

    Code (CSharp):
    1. [UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup))]
    2. [UpdateAfter(typeof(CubeMovementSystem))]
    Now, when following, it's mostly in-sync, but it jitters around 1-2 times per second.
    I've tried recording a video but without the debug bounding boxes the jitters are so fast it's hard to see in the video.


    However, It's not subtle and is very disruptive when actually moving around in play-mode.

    What would be causing this issue?

    Here's the current CameraSystem:

    Code (CSharp):
    1. [UpdateInGroup(typeof(PredictedFixedStepSimulationSystemGroup))]
    2. [UpdateAfter(typeof(CubeMovementSystem))]
    3. public partial class CameraSystem : SystemBase {
    4.     protected override void OnUpdate() {
    5.         // return; // DEBUG
    6.  
    7.         float3 player_world_position = float3.zero;
    8.  
    9.         foreach (var (
    10.             _,
    11.             player,
    12.             local_transform,
    13.             local_to_world,
    14.             entity
    15.         ) in SystemAPI.Query<
    16.             GhostOwnerIsLocal,
    17.             RefRO<Player>,
    18.             RefRO<LocalTransform>,
    19.             RefRO<LocalToWorld>
    20.         >().WithEntityAccess()) {
    21.             player_world_position = local_transform.ValueRO.Position;  // World position
    22.         }
    23.  
    24.         foreach (var (
    25.             camera,
    26.             camera_settings,
    27.             local_to_world
    28.         ) in SystemAPI.Query<
    29.             MainCamera,
    30.             RefRW<MainCameraSettings>,
    31.             RefRW<LocalToWorld>
    32.         >()) {
    33.             float3 player_camera_position = SystemAPI.GetComponent<LocalToWorld>(camera_settings.ValueRO.Position).Position;
    34.             float3 current_position = camera.Transform.position;
    35.             Debug.Log($"Camera Cur Pos: {current_position}");
    36.             Debug.Log($"Camera New Pos: {player_camera_position}");
    37.  
    38.             camera.Transform.position = player_camera_position;
    39.             camera.look_at = player_world_position;
    40.             camera.Transform.LookAt(player_world_position);
    41.         }
    42.     }
    43. }
     
    Last edited: Dec 8, 2023
  5. SamuelBellomoUnity

    SamuelBellomoUnity

    Unity Technologies

    Joined:
    Sep 24, 2020
    Posts:
    8
    Your Movement component has the GhostComponent attribute, but has no GhostFields, so it's actually not replicated (if you look at your source generated folder, Movement should not be there). Make sure to mark your fields as [GhostField]. (It's not super clear in our docs and I got caught by this too originally, I'll add a task to fix this on our side).
    So your Movement component won't get rolled back when resimulating and won't get synced with the server correctly. That could explain some of the discrepancy.
    And again make sure your logic runs before Physics' simulation group.
     
    NikiWalker likes this.