Closed michalChrobot closed 6 months ago
To organize it a little bit I wrote some prerequisites I want to implement
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
I added several different things.
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
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
}
}
Right now the situation looks like this:
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.