michalooo / Thesis-Deterministic-Lockstep-Netcode-DOTS-with-Validation

Mitigating network latency in fast-paced multiplayer online games
0 stars 0 forks source link

Add fixed input latency #52

Closed michalChrobot closed 6 months ago

michalChrobot commented 7 months ago

Right now the situation looks like this:

  1. Server sends an RPC with a request to start the game and based on network conditions some players will get it faster then others which mean they will start 'ticking' sooner
  2. It leads to situation that in the same moment P1 sends his tick t60 and P2 sends the tick t40. Let's assume that P1 just moved
  3. After the inputs will be send to the server and confirmed (after P2 will also send his input for tick t60) it will be send back to players
  4. P1 will update the positions and send it (with different hash since he changed localtransorms for his t81 let's say). At the same time P2 will be sending his inputs with the modified hash for t61
  5. Here the problem happens since they will have the same localtransorms and hashes (the hash is calculated and send at the end of each client tick) but P1 will send it with his t81 and P2 with tick 61 which will lead to desync since server already revived info about t61 from player one and in it the hash was different because the position didn't change yet

    The solution is to add fixed input latency of around 100-200ms (at 60Hz, that's 6 or 12). In the case of this package let's set it to 9

Thus players will be sending inputs for tick t+10, but they must still PAUSE if they will stop receiving inputs from other players.

michalChrobot commented 7 months ago

To organize it a little bit I wrote some prerequisites I want to implement

michalChrobot commented 7 months ago

Regarding step 4 I decided to just set currentTick to equal tickAhead so we will have few empty places in the array but its super few

michalChrobot commented 7 months ago

I added several different things.

  1. System groups and order changed
    
    ConnectionHandleSystemGroup
    ClientBehaviour --> here I will update my table with potentiall updates from the server

SpawnPlayerSystem CalculateTickSystem --> This System is responsible for managing ticks and checking if we should update positions and send ticks

DeterministicSimulationSystemGroup DeterministicPresentationSystemGroup PlayerUpdateSystem DeterminismSystemCheck PlayerInputGatherAndSendSystem


2. I added CalculateTickSystem to manage this time check

[UpdateAfter(typeof(SpawnPlayerSystem))] [UpdateBefore(typeof(DeterministicSimulationSystemGroup))] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] public partial class CalculateTickSystem : SystemBase { protected override void OnCreate() { RequireForUpdate(); // delete }

protected override void OnUpdate()
{
    var deltaTime = SystemAPI.Time.DeltaTime;

    foreach (var (tickRateInfo, storedTicksAhead, connectionEntity) in SystemAPI.Query<RefRW<TickRateInfo>, RefRW<StoredTicksAhead>>().WithAll<GhostOwnerIsLocal, PlayerSpawned>().WithEntityAccess())
    {
        tickRateInfo.ValueRW.delayTime -= deltaTime;
        if (tickRateInfo.ValueRO.delayTime <= 0)
        {
            if (tickRateInfo.ValueRO.currentClientTickToSend <= tickRateInfo.ValueRO.tickAheadValue)
            {
                tickRateInfo.ValueRW.currentClientTickToSend++;
                EntityManager.SetComponentEnabled<PlayerInputDataToSend>(connectionEntity, true);
            }
            else
            {
                for(int i=0; i<storedTicksAhead.ValueRO.entries.Length; i++)
                {
                    if(storedTicksAhead.ValueRO.entries[i].tick == tickRateInfo.ValueRO.currentSimulationTick + tickRateInfo.ValueRO.tickAheadValue) // Here the only problem would be if let's say 12 inputs arrived before the next one and our array is full
                    {
                        tickRateInfo.ValueRW.currentClientTickToSend++;
                        tickRateInfo.ValueRW.currentSimulationTick++;

                        UpdateComponentsData(storedTicksAhead.ValueRO.entries[i].data);
                        storedTicksAhead.ValueRW.entries[i].Dispose();
                        storedTicksAhead.ValueRW.entries[i] = new InputsFromServerOnTheGivenTick { tick = 0 };
                        EntityManager.SetComponentEnabled<PlayerInputDataToSend>(connectionEntity, true);
                        EntityManager.SetComponentEnabled<PlayerInputDataToUse>(connectionEntity, true); // We are assuming that client input to Send will be always x ticks in from of the simulation one

                        break;
                    }
                }
            }

            tickRateInfo.ValueRW.delayTime = 1f / tickRateInfo.ValueRO.tickRate;
        }
    }
}

void UpdateComponentsData(RpcPlayersDataUpdate rpc)
{
    // Update player data based on received RPC
    NativeList<int> networkIDs = new NativeList<int>(16, Allocator.Temp);
    NativeList<Vector2> inputs = new NativeList<Vector2>(16, Allocator.Temp);
    networkIDs = rpc.NetworkIDs;
    inputs = rpc.Inputs;

    foreach (var (playerInputData, connectionEntity) in SystemAPI
                 .Query<RefRW<PlayerInputDataToUse>>()
                 .WithOptions(EntityQueryOptions.IgnoreComponentEnabledState)
                 .WithAll<PlayerInputDataToSend>().WithEntityAccess())
    {
        var idExists = false;
        for (int i = 0; i < networkIDs.Length; i++)
        {
            if (playerInputData.ValueRO.playerNetworkId == networkIDs[i])
            {
                idExists = true;
                playerInputData.ValueRW.horizontalInput = (int)inputs[i].x;
                playerInputData.ValueRW.verticalInput = (int)inputs[i].y;
                EntityManager.SetComponentEnabled<PlayerInputDataToUse>(connectionEntity, true);
                EntityManager.SetComponentEnabled<PlayerInputDataToSend>(connectionEntity, false);
            }
        }

        if (!idExists) //To show that the player disconnected
        {
            playerInputData.ValueRW.playerDisconnected = true;
            EntityManager.SetComponentEnabled<PlayerInputDataToUse>(connectionEntity, true);
            EntityManager.SetComponentEnabled<PlayerInputDataToSend>(connectionEntity, false);
        }
    }
}

}


3. TickRateInfo component was updated to keep the values of currentSimulationTick and currentClientTickToSend

struct TickRateInfo : IComponentData { public int tickRate; public int tickAheadValue;

public float delayTime;
public int currentSimulationTick; // Received simulation tick from the server
public int currentClientTickToSend; // We are sending input for the tick in the future
public ulong hashForTheTick;

}


4. ClientBehaviour upon reciving the data is just saving it in the array

void UpdatePlayersData(RpcPlayersDataUpdate rpc) // When do I want to refresh the screen? When input from the server arrives or together with the tick?? { Debug.Log("updating"); // Update player cubes based on received data, I need a job that for each component of type Player will enable it and change input values there // Enable component on player which has info about current position of the player // Create a characterController script on player which will check if this component is enabled and then update the position of the player and disable that component

    foreach (var storedTicksAhead in SystemAPI.Query<RefRW<StoredTicksAhead>>().WithAll<GhostOwnerIsLocal>())
    {
        bool foundEmptySlot = false;
        for (int i = 0; i < storedTicksAhead.ValueRW.entries.Length; i++)
        {
            Debug.Log("elo " + storedTicksAhead.ValueRO.entries[i].tick);
            if (storedTicksAhead.ValueRO.entries[i].tick == 0) // Check if the tick value is 0, assuming 0 indicates an empty slot
            {
                storedTicksAhead.ValueRW.entries[i] = new InputsFromServerOnTheGivenTick { tick = rpc.Tick, data = rpc };
                foundEmptySlot = true;
                break; // Exit the loop after finding an empty slot
                // Packages can be unreliable so it's better to give them random slot and later check the tick
            }
        }

        if (!foundEmptySlot)
        {
            Debug.LogError("No empty slots available to store the value of a future tick from the server. " + "The current capacity is: " + storedTicksAhead.ValueRO.entries.Length + " This error means an implementation problem");
            for (int i = 0; i < storedTicksAhead.ValueRW.entries.Length; i++)
            {
                Debug.LogError("One of the ticks is " + storedTicksAhead.ValueRO.entries[i].tick);
            }
        }
        // Always current tick is less or equal to the server tick and the difference between them can be max tickAhead
    }
}