Inputs from client to server appear to be lost/dropped very often

Hi,

We’re facing an issue that sounds very similar to the one presented and discussed here: Inputs occasionally missed or processed twice on the server.

Basically, we’ve managed to reproduce a bug with a very minimal setup that causes inputs (IInputComponentData value changes) to not be picked up/seen/received by the server. Here’s the minimal project zipped up: NetcodeBootstrap.zip - Google Drive.

The issue is reproducible even when running the server & client on the same machine, all inside the editor - i.e. with latency of ~10-15ms (which also seems a bit high for running it all locally, but that’s a topic for another day).

We have a few components (simplified slightly here):

    public struct TestInputComponent : IInputComponentData
    {
        public float Value;
    }

    [GhostComponent(PrefabType = GhostPrefabType.All)]
    public struct TestGhostComponent : IComponentData
    {
        [GhostField]
        public float Value;
    }

A very simple input system:

    [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
    [UpdateInGroup(typeof(GhostInputSystemGroup))]
    public partial class TestInputSystem : SystemBase 
    {
        protected override void OnUpdate()
        {
            foreach (var (input, ghost) in SystemAPI.Query<RefRW<TestInputComponent>, RefRO<TestGhostComponent>>())
            {
                if (Input.GetKeyUp(KeyCode.Space))
                {
                    input.ValueRW.Value = 10f;
                    Log.Info($"Client input set to {input.ValueRO.Value} now, current ghost value: {ghost.ValueRO.Value} (new intended value: {ghost.ValueRO.Value + input.ValueRW.Value})");
                }
                else
                {
                    input.ValueRW.Value = 0f;
                }
            }
        }
    }

And a very simple input handler system:

    [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation | WorldSystemFilterFlags.ClientSimulation)]
    [UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
    public partial struct TestHandleInputSystem : ISystem
    {
        public void OnUpdate(ref SystemState state)
        {
            state.Dependency = new ApplyJob()
                .Schedule(state.Dependency);
        }

        [WithAll(typeof(Simulate))]
        private partial struct ApplyJob : IJobEntity
        {
            private void Execute(in TestInputComponent input, ref TestGhostComponent ghost)
            {
                ghost.Value += input.Value;
            }
        }
    }

Beyond that there is some boilerplate code to spawn in an authored ‘prefab’, do some more debug logging and display the component values to UI, but those are just for being able to run the test and debug. The code has been tested as is presented above, even if the zipped project has slightly more code.

Here’s the authored prefab with its GhostAuthoringComponent:
(see next comment, I can only add one embed apparently)

It works as intended most of the time, but somewhere between 5% and 30% of the time the input is simply not handled on the server. With simple debug logging we can compare the ‘normal intended’ flow (green) with the error flow (red):
(see next comment, I can only add one embed apparently)

Notice how the ClientWorld updates are followed by a single ServerWorld update in the ‘OK!’ scenario, but not in the error scenario.
Since we’re testing with server & client on the same machine, we are assuming that packet drops are not supposed to occur - is that fair to assume?

Tested in Unity 6000.0.27f1.
Entities version 1.3.5.
Netcode for entities version 1.3.6.

So what gives? Please help! Any insight is appreciated, as we are failing to understand why or how this happens.

The missing embeds:

It’s the same issue as the one you’ve linked. We are currently investigating and should hopefully have a fix soon.

1 Like

For those interested, we managed to “work around” this issue by implementing the solution proposed here: How to best implement client authoritative behaviour - #2 by ts_headfirst

In essence, the client has its own field which it accumulates on an input component - it is never reset.
The server also has a field on a ghost component - it is also never reset.
Calculating the actual health becomes: CurrentHealth + (ClientAccumulatedDelta - ServerAccumulatedDelta).

This works reliably and seems to be robust, but is more involved and requires sending more data - compared to if the inputs were actually guaranteed to reach the server, as one might expect.