How to handle non-bool InputEvent

Problem

  • We have IInputComponentData with an InputEvent which we use for storing a boolean input. This protects us when sampling input due to its Count tracking and some (to me) behind the scenes magic:
public struct PlayerInput : IInputComponentData
{
    public InputEvent Jump;
}
        /// <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;

Great, but now I have input that’s not a boolean which I want the same protection for. Specifically, I have selected a unit to play with an identifier. I cant use InputEvent because its not a boolean.

public struct PlayerInput : IInputComponentData
{
    public int PlayUnitId;
}

I now risk the input being reset if the GhostSystemGroup runs multiple times before the CommandSendSystemGroup runs for a tick since theoretically I only register inputs once. I also found that the CommandSendSystemGroup will sometimes run and send a packet for a tick even if the GhostInputSystemGroup (and client simulation) did not run resulting in the last input being reused and getting double processed on the server.

Essentially, im losing what it appears IInputComponentData/InputEvent protected from. Which is ensuring inputs are only processed once per Set() and dont get reset.

Question
So, what is the recommended way to send non-boolean inputs?

Im aware I could send guaranteed one-off events using RPCs, but I would like to use client prediction when spawning/targeting specified units.

Other Info
Ive tried using ICommandData myself and a custom utility function to not reset input when sampling multiple times per tick. But, still run into issues where my GhostInputSystemGroup system does not run for a tick on rare ticks, but
CommandSendSystem does and the server reuses the last input for the tick (as mentioned above).

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct PlayerInput : ICommandData
{
    public NetworkTick Tick;
    public int PlayUnitId;

    NetworkTick ICommandData.Tick
    {
        get => this.Tick;
        set => this.Tick = value;
    }
}

public static partial class CommandDataUtility
{
    public static void AddPlayerInputCommandData(this DynamicBuffer<PlayerInput> commandBuffer, PlayerInput commandData)
    {
        var foundData = commandBuffer.GetDataAtTick(commandData.Tick, out var existingBoardInput);
        if (!foundData || existingBoardInput.Tick != commandData.Tick)
        {
            Debug.Log($"First time setting input for tick {commandData.Tick.TickValue}");
            commandBuffer.AddCommandData(commandData);
            return;
        }

        Debug.Log($"Adjusting input for tick {commandData.Tick.TickValue}");

        // Check if values have been already set
        if (existingBoardInput.PlayUnitId > 0)
        {
            existingBoardInput.PlayUnitId = commandData.PlayUnitId;
        }

        commandBuffer.AddCommandData(existingBoardInput);
    }
}

[UpdateInGroup(typeof(GhostInputSystemGroup))]
partial struct PlayerInputSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<NetworkTime>();
    }

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

        var newPlayerInput = new PlayerInput();
        newPlayerInput.PlayUnitId = -1; // Default no unit played

        // Check for spawning units (only spawning one unit at a time)
        foreach (var (unit, ghost, unitEntity) in SystemAPI.Query<UnitComponent, GhostInstance>().WithAll<SpawningUnitTag, GhostOwnerIsLocal>().WithEntityAccess())
        {
            newPlayerInput.PlayUnitId = unit.unitId;
        }

        // Set input for tick
        foreach (var playerInputBuffer in SystemAPI.Query<DynamicBuffer<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
        {

            Debug.Log($"Sending input for tick {networkTime.ServerTick.TickValue} with play unit set as {newBoardInput.DrawCard}");
            newPlayerInput.Tick = networkTime.ServerTick;
            newPlayerInput.AddPlayerInputCommandData(newPlayerInput);
        }

        ecb.Playback(state.EntityManager);
    }
}

Bumping this as I am running into the same issue and can’t seem to figure out how it is meant to be done.

public struct UseAbilityInput : IInputComponentData
    {
        public int SlotIndex;
        public InputEvent UseAbility;
    }

For me, I record the int abilitySlot that the player wants to use as when I Set() the UseAbility event. Sometimes the SlotIndex has already been set back to its default value before the UseAbility event is sent to the server. Is there a way to guarantee that the server will read the SlotIndex value from my input component on the tick that the InputEvent is processed on the server?

edit:

I think i found the answer in another post. I needed to make sure that I was setting the UseAbility InputEvent to default every frame while sampling for input, but to keep the SlotIndex as its current value. Not sure if this is the best practice but I now get the same SlotIndex every time the InputEvent is read on the server.

Yes this is how you should do it. You reset input fields any time you read from input and set them to fields which should be sent to the servers. Non-bool ones don’t need specific handling because they are sent every tick and if missed , they resend for multiple ticks (i guess it was 3 and configurable) but bool events need to be sent ONLY ONCE when you press something so need special handling.