Search Unity

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

Question Odd GhostField/Rotation behavior

Discussion in 'NetCode for ECS' started by joshrs926, Nov 14, 2023.

  1. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    111
    I’m experiencing odd behavior with my client predicted player controller. When I move the mouse/gamepad right stick to rotate my view, after I stop moving the view moves back a tiny bit the direction it came from. It’s hard to tell but this may be happening constantly the whole time I rotate the player’s view. But I can see it for sure happens when I stop rotating the view. This only happens when the render frame rate is lower than the prediction rate. When the render rate is higher, it feels totally smooth. I managed to minimally reproduce this issue with the following code. When I run this code the object behaves the same way I described the player view. I've also attached a screenshot of the prefab that is running this test code. Perhaps I missed some important step when setting up the various components, systems, and prefab. Any ideas?

    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Mathematics;
    5. using Unity.NetCode;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8. using UnityEngine.InputSystem;
    9.  
    10. public struct TestGhostInput : IInputComponentData
    11. {
    12.     public float yawSpeed;
    13.     public int frame;
    14. }
    15.  
    16. public struct TestGhostRotation : IComponentData
    17. {
    18.     [GhostField]
    19.     public float yaw;
    20. }
    21.  
    22. public struct TestGhostSpawner : IComponentData
    23. {
    24.     public Entity prefab;
    25. }
    26.  
    27. [UpdateInGroup(typeof(GhostInputSystemGroup))]
    28. public partial class TestGhostInputSystem : SystemBase
    29. {
    30.     int frame = 1;
    31.    
    32.     protected override void OnUpdate()
    33.     {
    34.         foreach (var input in SystemAPI.Query<RefRW<TestGhostInput>>().WithAll<GhostOwnerIsLocal>())
    35.         {
    36.             float2 inputValue = Gamepad.current.rightStick.ReadValue();
    37.             input.ValueRW.yawSpeed = inputValue.x * 10;
    38.             input.ValueRW.frame = frame;
    39.             Debug.Log($"Input {frame} | speed {input.ValueRW.yawSpeed}");
    40.         }
    41.         frame++;
    42.     }
    43. }
    44.  
    45. [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
    46. public partial struct TestGhostRotationSystem : ISystem
    47. {
    48.     public void OnUpdate(ref SystemState state)
    49.     {
    50.         foreach (var (input, rotation) in SystemAPI.Query<TestGhostInput, RefRW<TestGhostRotation>>().WithAll<Simulate>())
    51.         {
    52.             rotation.ValueRW.yaw += input.yawSpeed * SystemAPI.Time.DeltaTime;
    53.             string clientServerLog = state.World.IsServer() ? "Server" : "Client";
    54.             Debug.Log($"Predict.{clientServerLog} {input.frame} | speed {input.yawSpeed} | yaw {rotation.ValueRW.yaw} | deltaTime {SystemAPI.Time.DeltaTime}");
    55.         }
    56.     }
    57. }
    58.  
    59. [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
    60. [UpdateAfter(typeof(TestGhostRotationSystem))]
    61. public partial struct TestGhostTransformSystem : ISystem
    62. {
    63.     public void OnUpdate(ref SystemState state)
    64.     {
    65.         foreach (var (rotation, transform) in SystemAPI.Query<TestGhostRotation, RefRW<LocalTransform>>().WithAll<Simulate>())
    66.         {
    67.             transform.ValueRW.Rotation = quaternion.EulerYXZ(0, rotation.yaw, 0);
    68.         }
    69.     }
    70. }
    71.  
    72. [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
    73. public partial struct TestGhostSpawnerServerSystem : ISystem
    74. {
    75.     public void OnCreate(ref SystemState state)
    76.     {
    77.         state.RequireForUpdate<TestGhostSpawner>();
    78.         state.RequireForUpdate<NetworkStreamInGame>();
    79.     }
    80.    
    81.     public void OnUpdate(ref SystemState state)
    82.     {
    83.         TestGhostSpawner spawner = SystemAPI.GetSingleton<TestGhostSpawner>();
    84.         EntityCommandBuffer ecb = new(Allocator.Temp);
    85.         foreach (var networkId in SystemAPI.Query<NetworkId>())
    86.         {
    87.             Entity instance = ecb.Instantiate(spawner.prefab);
    88.             ecb.SetComponent(instance, new GhostOwner() { NetworkId = networkId.Value });
    89.         }
    90.         ecb.Playback(state.EntityManager);
    91.         state.Enabled = false;
    92.     }
    93. }
    94.  
    95. using Unity.Entities;
    96. using UnityEngine;
    97.  
    98. public class TestGhostAuthoring : MonoBehaviour
    99. {
    100.     class Baking : Baker<TestGhostAuthoring>
    101.     {
    102.         public override void Bake(TestGhostAuthoring authoring)
    103.         {
    104.             Entity entity = GetEntity(TransformUsageFlags.Dynamic);
    105.             AddComponent(entity, new TestGhostInput());
    106.             AddComponent(entity, new TestGhostRotation());
    107.         }
    108.     }
    109. }
    110.  
    111. using Unity.Entities;
    112. using UnityEngine;
    113.  
    114. public class TestGhostSpawnerAuthoring : MonoBehaviour
    115. {
    116.     public GameObject Prefab;
    117.  
    118.     public class PositionerSpawnerBaker : Baker<TestGhostSpawnerAuthoring>
    119.     {
    120.         public override void Bake(TestGhostSpawnerAuthoring authoring)
    121.         {
    122.             var entity = GetEntity(TransformUsageFlags.Dynamic);
    123.             AddComponent(entity, new TestGhostSpawner() { prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic) });
    124.         }
    125.     }
    126. }

    upload_2023-11-14_15-26-13.png
     
    Last edited: Nov 17, 2023
  2. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    111
    After logging some data to the console every frame I can see that the server and client are getting out of sync. I bet the camera jittering is just the client getting corrected by new server values. I am running the client and server on the same machine in the Unity Editor on my PC. I'm using Application.targetFrameRate to set the frame rate to something low like 20 to induce this behavior. I notice that the server then appears to run at that rate too. At the same time I set the ClientServerTickRate.SimulationTickRate to 60. Why would the server be running at the lower frame rate in that case?
    My ultimate goal is to have smooth first person player controls in a server authoritative game. Does any one have any ideas here?
     
    Last edited: Nov 16, 2023
  3. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    111
    Here is a screenshot of logs I printed out. You can see the first Input log shows some input then the rest of the input logs show 0. This is me letting go of the right stick. When I do that, the final client prediction log of each frame shows that it is correctly handling the latest input. But the corresponding server values for the same frame don't end up the same as what the client predicted. They are less. So then the client reduces the value over a few frames to match the server. I, as the player, experience this as me pushing the right stick right to rotate the camera to the right, then I let go and the camera jerks left just a bit before being still. Hopefully someone has some experience with this and can help me here. Making a smooth first person camera is critical for me.
    upload_2023-11-16_20-27-41.png
     
  4. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    287
    Hey joshrs926,

    Thanks for the report, we will take a look. I think I remember occasionally seeing this too, in the editor. Can you file a bug report via the editor, for tracking purposes, please? Cheers.