using System;
using System.Diagnostics;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.NetCode.LowLevel.Unsafe;
namespace Unity.NetCode.Samples
{
///
/// System used to restore the state of the client-only component present on predicted ghost when a
/// new snapshot from server is received.
///
/// The system must run after the (responsible
/// to update the state of all ghosts) and before the
/// to guarantee the predicted ghosts components states are all synced to the same last received tick.
///
/// The restoring process copy the component data and enable bits from the for
/// all ghosts that received a new snapshot.
/// If the server for which we want to restore the data is not found in the backup buffer, the components data are
/// left unchanged.
///
///
/// After the component data has been restore for the last received tick, all the client-only buffers
/// are shrank, by removing the oldest backup. In particular:
/// - For the ghosts that has received the new snapshot, the buffer is cleared
/// - For all ghosts that were not present in the snapshot, all backup with a tick older than the last received tick are removed.
///
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
[UpdateAfter(typeof(GhostUpdateSystem))]
public partial struct ClientOnlyComponentRestoreSystem : ISystem
{
private EntityQuery predictedGhostsWithClientOnlyBackup;
private EntityStorageInfoLookup childEntityLookup;
private BufferTypeHandle linkedEntityGroupHandle;
private ComponentTypeHandle backupTypeHandle;
private ComponentTypeHandle ghostTypeHandle;
private ComponentTypeHandle predictedGhostTypeHandle;
//Internal to make it accessible by the tests
internal ClientOnlyTypeHandleList componentTypeHandles;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var queryBuilder = new EntityQueryBuilder(Allocator.Temp);
queryBuilder.WithAll();
queryBuilder.WithAll();
queryBuilder.WithAll();
predictedGhostsWithClientOnlyBackup = state.GetEntityQuery(queryBuilder);
linkedEntityGroupHandle = state.GetBufferTypeHandle(true);
childEntityLookup = state.GetEntityStorageInfoLookup();
backupTypeHandle = state.GetComponentTypeHandle();
ghostTypeHandle = state.GetComponentTypeHandle(true);
predictedGhostTypeHandle = state.GetComponentTypeHandle(true);
state.RequireForUpdate();
state.RequireForUpdate();
state.RequireForUpdate();
state.RequireForUpdate(predictedGhostsWithClientOnlyBackup);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var clientOnlyCollection = SystemAPI.GetSingleton();
var networkTime = SystemAPI.GetSingleton();
// complete the dependency in order to access the NetworkSnapshotAck (because written in NetworkStreamReceiveSystem)
state.CompleteDependency();
var ackComponent = SystemAPI.GetSingleton();
backupTypeHandle.Update(ref state);
ghostTypeHandle.Update(ref state);
predictedGhostTypeHandle.Update(ref state);
childEntityLookup.Update(ref state);
linkedEntityGroupHandle.Update(ref state);
componentTypeHandles.CreateOrUpdateTypeHandleList(ref state, clientOnlyCollection.ClientOnlyComponentTypes.AsArray());
//copy the component data from backup buffer and clear the client only backup buffers based on
//on the last received snapshot tick.
var job = new RestoreFromBackup
{
ghostTypeHandle = ghostTypeHandle,
predictedGhostComponentTypeHandle = predictedGhostTypeHandle,
linkedEntityGroupHandle = linkedEntityGroupHandle,
childEntityLookup = childEntityLookup,
backupTypeHandle = backupTypeHandle,
componentTypeHandles = componentTypeHandles,
clientOnlyComponentCollection = clientOnlyCollection.BackupInfoCollection.AsArray().AsReadOnly(),
prefabMetadata = clientOnlyCollection.GhostTypeToPrefabMetadata.AsReadOnly(),
serverTick = networkTime.ServerTick,
lastReceivedSnapshotByLocal = ackComponent.LastReceivedSnapshotByLocal,
netDebug = SystemAPI.GetSingleton()
};
state.Dependency = job.ScheduleParallel(predictedGhostsWithClientOnlyBackup, state.Dependency);
}
[BurstCompile]
unsafe struct RestoreFromBackup : IJobChunk
{
[ReadOnly] public ComponentTypeHandle ghostTypeHandle;
[ReadOnly] public ComponentTypeHandle predictedGhostComponentTypeHandle;
[ReadOnly] public BufferTypeHandle linkedEntityGroupHandle;
public ComponentTypeHandle backupTypeHandle;
public EntityStorageInfoLookup childEntityLookup;
public ClientOnlyTypeHandleList componentTypeHandles;
public NativeArray.ReadOnly clientOnlyComponentCollection;
public NativeHashMap.ReadOnly prefabMetadata;
public NetworkTick serverTick;
//The latest received tick from the server. All backup history before that tick can be cleared
public NetworkTick lastReceivedSnapshotByLocal;
public NetDebug netDebug;
public void Execute(in ArchetypeChunk chunk, int chunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
{
var ghostComponents = chunk.GetNativeArray(ref ghostTypeHandle);
var ghostType = ghostComponents[0];
//Early exit if type is not setup or the ghost type is not found yet in the backup info.
if (!prefabMetadata.TryGetValue(ghostType, out var metadata))
{
netDebug.LogError($"Unable to find client-only backup metadata for ghost with ghost type {ghostType}");
return;
}
var predictedGhosts = chunk.GetNativeArray(ref predictedGhostComponentTypeHandle);
//Use unsafe pointer to avoid the local copy and to let access the backup data by ref
var backups = (ClientOnlyBackup*)chunk.GetNativeArray(ref backupTypeHandle).GetUnsafeReadOnlyPtr();
for (int ent = 0; ent < chunk.Count; ++ent)
{
//This is the state we need to restore from. Can be:
// - the last full prediction tick (in case the prediction is continuing)
// - the last received tick (in case of new data)
// - the current tick (so nothing do)
var predictionStartTick = predictedGhosts[ent].PredictionStartTick;
//If the backup didn't run yet or the entity is just spawned, use the current state.
//IF the start tick is the the current target tick, there is nothing to backup.
if (backups[ent].IsEmpty || !predictionStartTick.IsValid || predictionStartTick == serverTick)
continue;
var backupSlotIndex = backups[ent].GetSlotForTick(predictionStartTick, metadata.backupSize);
//if there is no backup available do nothing and use the current component state
if (backupSlotIndex < 0)
continue;
var bufferReader = new BackupReader(backupSlotIndex, backups[ent], metadata);
RestoreComponentsFromBackup(chunk, ref bufferReader, ent, metadata);
//Reset the length of the buffer to 0. We are going to re-predict all the ticks from the the prediction start
//to for this entity
if(predictionStartTick == lastReceivedSnapshotByLocal)
backups[ent].Clear();
}
//Remove all backup with tick less or equal than the last received tick from the server.
//Ghosts are not not going to rollback to this tick anymore.
for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent)
backups[ent].RemoveBackupsOlderThan(lastReceivedSnapshotByLocal, metadata.backupSize);
}
private void RestoreComponentsFromBackup(ArchetypeChunk chunk, ref BackupReader reader, int ent,
in ClientOnlyBackupMetadata metadata)
{
//We have a valid backup slot to use. Restore components and buffers.
var iterEnd = metadata.componentBegin + metadata.numRootComponents;
var compIdx = metadata.componentBegin;
var typeHandles = new UnsafeList(componentTypeHandles.Ptr, clientOnlyComponentCollection.Length);
for (;compIdx < iterEnd; ++compIdx)
{
var comp = clientOnlyComponentCollection[compIdx];
var typeHandle = typeHandles[comp.ComponentIndex];
Assertions.Assert.IsFalse(comp.ComponentType.IsBuffer);
if (!chunk.Has(ref typeHandle))
{
reader.Skip(comp);
continue;
}
if (comp.ComponentType.IsEnableable)
{
var handle = typeHandle;
chunk.SetComponentEnabled(ref handle, ent, reader.IsEnabled(compIdx));
}
var compDataPtr = (byte*)chunk
.GetDynamicComponentDataArrayReinterpret(ref typeHandle, comp.ComponentSize)
.GetUnsafePtr();
compDataPtr += comp.ComponentSize * ent;
reader.RestoreComponent(comp, compDataPtr);
}
//If the chunk does not have a linked entity group we can't restore any child component.
if (!chunk.Has(ref linkedEntityGroupHandle))
return;
var entityGroup = chunk.GetBufferAccessor(ref linkedEntityGroupHandle);
for (var childCompIdx = compIdx; childCompIdx < metadata.componentEnd; ++childCompIdx)
{
var comp = clientOnlyComponentCollection[childCompIdx];
Assertions.Assert.IsFalse(comp.ComponentType.IsBuffer);
var typeHandle = typeHandles[comp.ComponentIndex];
//NOTE: this a safety condition in case the entity group is changed and some child removed.
//However, is a necessary but not sufficient condition: it is always possible to
//change the entity or removing and add entities and the length will be same.
//This does not provide a strong guarantee about which entity we are suppose to expect here,
//neither is archetype.
if (entityGroup[ent].Length <= comp.EntityIndex)
{
reader.Skip(comp);
continue;
}
var childEnt = entityGroup[ent][comp.EntityIndex].Value;
var childChunk = childEntityLookup[childEnt].Chunk;
var indexInChunk = childEntityLookup[childEnt].IndexInChunk;
if (!childChunk.Has(ref typeHandle))
{
reader.Skip(comp);
continue;
}
if (comp.ComponentType.IsEnableable)
{
var handle = typeHandle;
childChunk.SetComponentEnabled(ref handle, ent, reader.IsEnabled(childCompIdx));
}
var compDataPtr = (byte*)childChunk
.GetDynamicComponentDataArrayReinterpret(ref typeHandle, comp.ComponentSize)
.GetUnsafeReadOnlyPtr();
compDataPtr += comp.ComponentSize * indexInChunk;
reader.RestoreComponent(comp, compDataPtr);
}
}
//Little class that help reading backup from a buffer and that check boundary conditions (in the editor)
struct BackupReader
{
private readonly uint* enableBits;
private byte* compBackupPtr;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
readonly byte* beginCompDataPtr;
readonly int backupSize;
#endif
public BackupReader(int backupSlotIndex, in ClientOnlyBackup backup, in ClientOnlyBackupMetadata metadata)
{
var compDataSlotPtr = backup.ComponentBackup.Ptr + backupSlotIndex * metadata.backupSize;
var compDataOffset = GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + ClientOnlyBackup.EnableBitByteSize(metadata.componentEnd - metadata.componentBegin));
enableBits = (uint*)(compDataSlotPtr + sizeof(uint));
compBackupPtr = compDataSlotPtr + compDataOffset;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
beginCompDataPtr = compBackupPtr;
backupSize = metadata.backupSize;
#endif
}
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
private void CheckBounds()
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if((compBackupPtr - beginCompDataPtr) >= backupSize)
throw new ArgumentOutOfRangeException();
#endif
}
public void Skip(in ClientOnlyBackupInfo comp)
{
compBackupPtr += comp.ComponentSize;
CheckBounds();
}
public bool IsEnabled(int compIdx)
{
int bitIdx = 1 << (compIdx & 0x1f);
return (enableBits[compIdx >> 5] & bitIdx) != 0;
}
public void RestoreComponent(in ClientOnlyBackupInfo comp, byte *compDataPtr)
{
CheckBounds();
UnsafeUtility.MemCpy(compDataPtr, compBackupPtr, comp.ComponentSize);
compBackupPtr += comp.ComponentSize;
}
}
}
}
}