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);
    }
}
1 Like