Intermittent Server Misses IInputComponentData Tick

As a quick functional summary - I’m experimenting with a card game where clicking a board means one of two things:

  • Drawing a card if no card is selected
  • Play a card and create a token if a card is selected (and delete the card)

I’m using a single IInputComponent for all my Input data:

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct BoardInput : IInputComponentData
{
    [GhostField] public InputEvent DrawCard;
    [GhostField] public int PlayCardGhostId; // -1 means no played card
}

I set my input in a GhostInputSystemGroup ISystem:

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
        var networkTime = SystemAPI.GetSingleton<NetworkTime>();

        var newBoardInput = new BoardInput();
        newBoardInput.PlayCardGhostId = -1;

        var spawnedToken = false;

        foreach (var (board, clickedComponent, spriteRenderer, boardEntity) in SystemAPI.Query<RefRO<BoardTag>,
                                                                          RefRW<ClickedComponent>,
                                                                          RefRW<SpriteRendererComponent>>().WithEntityAccess())
        {         
            // Check for spawning cards
            foreach( var (card, ghost, cardEntity) in SystemAPI.Query<CardComponent, GhostInstance>().WithAll<SpawningCardTag, GhostOwnerIsLocal>().WithEntityAccess())
            {
                spawnedToken = true;
                newBoardInput.PlayCardGhostId = ghost.ghostId;
                Debug.Log($"NewBoardInput Play Card assigned with id {newBoardInput.PlayCardGhostId}");
            }

            // For now, if not spawning a token, then we are drawing a card
            if (!spawnedToken)
            {
                newBoardInput.DrawCard.Set();                
            }

            // Remove Process Clicked
            ecb.RemoveComponent<ClickedComponent>(boardEntity);
        }

        // Documentation claims Set() resets every frame, but thats actually wrong. InputEvent needs to be reset every frame:
        // https://discussions.unity.com/t/inputevent-does-not-fire-exactly-once/929531/4
        foreach (var boardInput in SystemAPI.Query<RefRW<BoardInput>>().WithAll<GhostOwnerIsLocal>())
        {
            if (spawnedToken)
            {
                Debug.Log($"Assigning board input play card ghost id - {newBoardInput.PlayCardGhostId} on tick {networkTime.ServerTick.TickValue}");
            }
            boardInput.ValueRW = newBoardInput;
        }

        ecb.Playback(state.EntityManager);
    }

I process the input in a PredictedSimulationSystemGroup ISystem:

    public void OnUpdate(ref SystemState state)
    {
        var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
        var networkTime = SystemAPI.GetSingleton<NetworkTime>();
        var DebugWorld = state.World.IsServer() ? "Server" : "Cient";
        if (!networkTime.IsFirstTimeFullyPredictingTick) return;

        foreach (var (boardInput, ghostOwner) in SystemAPI.Query<BoardInput, GhostOwner>().WithAll<Simulate>())
        {
            if (boardInput.PlayCardGhostId == -1)
            {
                Debug.Log("Play card ghost id is -1 or 0 so no card played");
                return;
            }

            Debug.Log($"Play Card {boardInput.PlayCardGhostId} set on tick {networkTime.ServerTick.TickValue}");

            // Get token for card
            foreach(var (playedCard, ghost, cardEntity) in SystemAPI.Query<CardComponent, GhostInstance>().WithAll<Simulate>().WithEntityAccess())
            {

                if (ghost.ghostId != boardInput.PlayCardGhostId) {
                    Debug.Log($"Ghost id {ghost.ghostId} does not equal played ghost id {boardInput.PlayCardGhostId} on tick {networkTime.ServerTick.TickValue} on {DebugWorld}");
                    continue; 
                }

                Debug.Log($"Play Card ghost id {ghost.ghostId} found ghost set on tick {networkTime.ServerTick.TickValue} on {DebugWorld}");

                var tokenEntity = ecb.Instantiate(playedCard.TokenPrefab);
                ecb.SetName(tokenEntity, "TokenEntity");
                ecb.SetComponent(tokenEntity, new GhostOwner { NetworkId = ghostOwner.NetworkId });

                // Place appopriately
                var spawnPosition = new float3(0, 0, -1); // TODO: Get mouse position
                ecb.SetComponent<LocalTransform>(tokenEntity, LocalTransform.FromPosition(spawnPosition));

                if (state.World.IsServer())
                {
                    ecb.AddComponent<DestroyEntityTag>(cardEntity);
                }                
            }
            
        }

        ecb.Playback(state.EntityManager);
    }

Problem
I’m experiencing an issue when playing a card where occasionally (about 15% of the time) the server appears to completely miss a played card which is triggered via PlayCardGhostId being set to a positive number.

“Its Working” logs:
(Both client and server see input)


(Just server sees input first - I expected client to be ahead of server on prediction so it surprises me to see no client processing, but it still works)
image

Problematic log:


(Above I see a Play card ghost id is -1 or 0 so no card played log for this tick)

Other Info:

  • All entities (card, token, board) are Owner Predicted ghosts with the same settings:
  • My BoardInput InputEvent DrawCard never misses and is also a “one off” event. The difference being PlayCardGhostId is just an int. There is a comment in my GhostInputSystemGroup system about needing to set new BoardInput(); on every frame.
  • Im unsure if using the ghostId to reference a played card is proper, but it was my first attempt. Regardless, the issue isn’t finding the associated ghost on the server, its that the PlayCardGhostId simply seems like its never set on the expected server tick.

Didn’t follow entirely but noticed this comment. This leads me to believe you process Input every frame but also share it across the network.

The way this works with NetworkVariable - in case this is the same issue here - is that if you change the value in frame #1 and it gets reset in frame #2 (or even the same frame) but the NetworkTickRate is “every 5 frames” then only the value that is set at the time the message is sent out will be sent.

Thus you may observe the following sequence on the server:

  • value=1
  • value=0
  • (network tick)
  • value=1
  • value=0
  • value=1
  • (network tick)

To be received on client-side as:

  • value=0
  • value=1

Might this be the issue?

If so you must not reset the Input system state, or at least not any “pressed” flags until after the network tick happens.

I think the pattern really is to reset the input data on every frame.

inputData = default;

if (jump)
    inputData.Jump.Set();
if (left)
    inputData.Horizontal -= 1;

Also see the character controller samples:

TBH I find IInputComponentData confusing mechanically and I feel like the documentation could be a bit more friendly here.

By using the InputEvent type within IInputComponentData inputs, you can guarantee that one-off events (such as those gathered by UnityEngine.Input.GetKeyDown) are synchronized properly with the server and registered exactly once, even when the exact input tick where the input event was first registered is dropped on its way to the server.

How it works

In a standard input component data struct you’ll have these systems set up:

  • Gather input system (client loop)
    • Take input events and save them in the input component data. This happens in GhostInputSystemGroup.
  • Process input system (server or prediction loop)
    • Take current input component and process the values. This usually happens in PredictedSimulationSystemGroup.

With IInputComponentData handling it looks like this with code-generated systems:

  • Gather input system (client loop)
    • Take input events and save them in the input component data. This happens in GhostInputSystemGroup.
  • Copy input to command buffer (client loop)
    • Take current input data component and add to command buffer, also recording current tick.
  • Apply inputs for current tick to input component data (server or prediction loop)
    • Retrieve inputs from command buffer for current tick and apply to input component. With prediction multiple input values could be applied as prediction rolls back (see Prediction).
  • Process input system (server or prediction loop)
    • Take current input component and process the values. This usually happens in PredictedSimulationSystemGroup.

The first and last steps are the same as with the single-player input handling, and these are the only systems you need to write/manage. An important difference, with netcode-enabled input, is that the processing system can be called multiple times per tick as previous ticks (rollback) are handled.

Specifically I have a hard time figuring out:

  • Why is InputEvent backed by an int and different from a simple bool? Why not a simple inputEvent.Set(bool wasPressedThisFrame) API? If I always reset the event before incrementing it, doesn’t that imply that the value is always 0 or 1? Below comment offers a hint but I still don’t get it
/// <summary>
/// Track if the event has been set for the current frame.
/// </summary>
/// <remarks> This could be higher than 1 when the inputs are sampled multiple times
/// before the input is sent to the server. Also if the input is sampled again before
/// being transmitted the set event will not be overridden to the unset state (count=0).
/// </remarks>
public uint Count;
  • Or is there some magic codegen stuff going on behind the scenes and that’s where the incrementable int is needed?

I kinda feel like I actually knew this at some point, forgot it, and now I’m having a hard time reconstructing the knowledge even with the docs on hand.

Ah. Yes it seems like I am resetting the input before it gets sent to the server as @CodeSmile noted.

I guess this is the value of InputEvent where the Count is kept so it cant be unset and one can check the count itself if multiple triggers matter. I also see in the Networked Cube example they increment or decrement values: Networked cube | Netcode for Entities | 1.4.0

playerInput.ValueRW = default;
            if (Input.GetKey("left"))
                playerInput.ValueRW.Horizontal -= 1;

This still leaves me a little confused though. If input is reset on every frame (playerInput.ValueRW = default;) then why would incrementing/decrementing matter (same confusion that @apkdev had I believe) ? It feels like the networked cube example would run into a similar issue as mine, but not as severe because rollback can handle slight position differences elegantly if an input is missed on the server as opposed to my one off “play card” event.

Also, wouldn’t InputEvent also have to be doing some magic to not completely lose its context between resetting of IInputComponentData every frame?

Im still unsure how I should handle my “playing a card/spawning a token” event:

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct BoardInput : IInputComponentData
{
    [GhostField] public int PlayCardGhostId;
}

Might this be the issue?
If so you must not reset the Input system state, or at least not any “pressed” flags until after the network tick happens.

  • It feels wrong to have the client wait to reset input until we receive a snapshot for that tick? What if the player played another card between that time?
  • Maybe I should be storing some sort of buffer of cards played that gets reset once input is sent to the server… but, i’m still unsure how to handle that with the pattern appearing to prescribe resetting input every frame (as I noted above).

The return here seems sus, are you sure you didn’t mean continue?
(I fell for this one so many times…)


Note that according to the docs the input system already is storing a buffer of inputs, paired with the current tick, in order to guarantee that actions are handled exactly once. That’s the theory anyway.

I think this is mainly supposed to apply to action game inputs such as shooting, jumping, etc. where prediction and rollback are important. If your game is turn-based, you can consider using RPCs for communication.

Unlike ghost SnapshotData, rpc messages are sent using a dedicated reliable channel and are therefore guaranteed to be received.

Hm. My game isn’t turn-based. Its a “card game”, but still real-time. I considered using RPCs for crucial inputs which involve spawning like drawing a card (creating a card entity) and playing a card (creating a token entity and destroying a card entity).

It should essentially be the same idea as any RTS though? Where I would like to predict the spawn after playing a card and correct once the server tries to play it.

        if (boardInput.PlayCardGhostId == -1)
        {
            Debug.Log("Play card ghost id is -1 or 0 so no card played");
            return;
        }

Yeah this should be a continue :sweat_smile: . I just haven’t noticed it yet because ive been testing with 1 player.