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?
Minimal Reproduction Code
using Unity.Collections;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
using UnityEngine;
using UnityEngine.InputSystem;
public struct TestGhostInput : IInputComponentData
{
public float yawSpeed;
public int frame;
}
public struct TestGhostRotation : IComponentData
{
[GhostField]
public float yaw;
}
public struct TestGhostSpawner : IComponentData
{
public Entity prefab;
}
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class TestGhostInputSystem : SystemBase
{
int frame = 1;
protected override void OnUpdate()
{
foreach (var input in SystemAPI.Query<RefRW<TestGhostInput>>().WithAll<GhostOwnerIsLocal>())
{
float2 inputValue = Gamepad.current.rightStick.ReadValue();
input.ValueRW.yawSpeed = inputValue.x * 10;
input.ValueRW.frame = frame;
Debug.Log($"Input {frame} | speed {input.ValueRW.yawSpeed}");
}
frame++;
}
}
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
public partial struct TestGhostRotationSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach (var (input, rotation) in SystemAPI.Query<TestGhostInput, RefRW<TestGhostRotation>>().WithAll<Simulate>())
{
rotation.ValueRW.yaw += input.yawSpeed * SystemAPI.Time.DeltaTime;
string clientServerLog = state.World.IsServer() ? "Server" : "Client";
Debug.Log($"Predict.{clientServerLog} {input.frame} | speed {input.yawSpeed} | yaw {rotation.ValueRW.yaw} | deltaTime {SystemAPI.Time.DeltaTime}");
}
}
}
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateAfter(typeof(TestGhostRotationSystem))]
public partial struct TestGhostTransformSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
foreach (var (rotation, transform) in SystemAPI.Query<TestGhostRotation, RefRW<LocalTransform>>().WithAll<Simulate>())
{
transform.ValueRW.Rotation = quaternion.EulerYXZ(0, rotation.yaw, 0);
}
}
}
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct TestGhostSpawnerServerSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<TestGhostSpawner>();
state.RequireForUpdate<NetworkStreamInGame>();
}
public void OnUpdate(ref SystemState state)
{
TestGhostSpawner spawner = SystemAPI.GetSingleton<TestGhostSpawner>();
EntityCommandBuffer ecb = new(Allocator.Temp);
foreach (var networkId in SystemAPI.Query<NetworkId>())
{
Entity instance = ecb.Instantiate(spawner.prefab);
ecb.SetComponent(instance, new GhostOwner() { NetworkId = networkId.Value });
}
ecb.Playback(state.EntityManager);
state.Enabled = false;
}
}
using Unity.Entities;
using UnityEngine;
public class TestGhostAuthoring : MonoBehaviour
{
class Baking : Baker<TestGhostAuthoring>
{
public override void Bake(TestGhostAuthoring authoring)
{
Entity entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new TestGhostInput());
AddComponent(entity, new TestGhostRotation());
}
}
}
using Unity.Entities;
using UnityEngine;
public class TestGhostSpawnerAuthoring : MonoBehaviour
{
public GameObject Prefab;
public class PositionerSpawnerBaker : Baker<TestGhostSpawnerAuthoring>
{
public override void Bake(TestGhostSpawnerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new TestGhostSpawner() { prefab = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic) });
}
}
}