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;
using Unity.NetCode.LowLevel.Unsafe;
namespace Unity.NetCode.Samples
{
///
/// System responsible for:
/// - creating the ClientOnlyCollection and the necessary metadata for processing ghosts with client-only components.
/// - add to all ghosts (that present or not the client-only components) a state component, the ClientOnlyBackup.
/// - backup the client-only components data for every full tick and store them inside the ClientOnlyBackup buffer.
///
/// System used to make a backup of all client-only component state. The system run at the end of the prediction loop,
/// and store inside the history buffer the state of components for each full predicted tick.
/// Partial ticks are not saved.
///
///
/// The backup consist of a mem-copy of the components data and, if the some of the component also implements
/// the , their enable bits.
///
///
/// The size of the buffer is not fixed and can grow,to accomodate both latency and
/// ghosts update frequency. The size of the buffer is still bounded, due to fact the oldest backup are removed and
/// the slot reused.
///
///
/// The saved components states are then used to restore the the components data when a new snapshot is received from the server.
/// See for more information.
///
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PredictedSimulationSystemGroup), OrderLast = true)]
[UpdateBefore(typeof(GhostPredictionHistorySystem))]
public partial struct ClientOnlyComponentBackupSystem : ISystem
{
private const int InitialBackupCapacity = 16;
private EntityQuery m_predictedGhostsWithClientOnlyBackup;
private EntityQuery m_predictedGhostsNotProcessed;
private EntityQuery m_predictedPrespawendGhostsNotProcessed;
private EntityQuery m_destroyedGhostsWithClientOnlyBackup;
private EntityStorageInfoLookup m_childEntityLookup;
private BufferTypeHandle m_linkedEntityGroupHandle;
private ComponentTypeHandle m_backupTypeHandle;
private ComponentTypeHandle m_ghostTypeHandle;
private NativeList m_clientOnlyComponentTypes;
private NativeList m_clientOnlyBackupInfoCollection;
private NativeHashMap m_ghostTypeToPrefabMetadata;
private ClientOnlyTypeHandleList m_clientOnlyTypeHandleList;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
var queryBuilder = new EntityQueryBuilder(Allocator.Temp);
queryBuilder.WithAll();
queryBuilder.WithAll();
queryBuilder.WithAll();
m_predictedGhostsWithClientOnlyBackup = state.GetEntityQuery(queryBuilder);
queryBuilder.Reset();
queryBuilder.WithAll();
queryBuilder.WithAll();
queryBuilder.WithNone();
queryBuilder.WithNone();
m_predictedGhostsNotProcessed = state.GetEntityQuery(queryBuilder);
queryBuilder.Reset();
queryBuilder.WithAll();
queryBuilder.WithAll();
queryBuilder.WithNone();
queryBuilder.WithAll();
m_predictedPrespawendGhostsNotProcessed = state.GetEntityQuery(queryBuilder);
queryBuilder.Reset();
queryBuilder.WithAll();
queryBuilder.WithNone();
m_destroyedGhostsWithClientOnlyBackup = state.GetEntityQuery(queryBuilder);
m_clientOnlyBackupInfoCollection = new NativeList(Allocator.Persistent);
m_clientOnlyComponentTypes = new NativeList(32, Allocator.Persistent);
m_ghostTypeToPrefabMetadata = new NativeHashMap(128, Allocator.Persistent);
m_clientOnlyTypeHandleList = default(ClientOnlyTypeHandleList);
m_backupTypeHandle = state.GetComponentTypeHandle();
m_ghostTypeHandle = state.GetComponentTypeHandle(true);
m_childEntityLookup = state.GetEntityStorageInfoLookup();
m_linkedEntityGroupHandle = state.GetBufferTypeHandle(true);
//Create the singleton.
var types = new NativeArray(1, Allocator.Temp);
types[0] = ComponentType.ReadWrite();
var singleton = state.EntityManager.CreateEntity(state.EntityManager.CreateArchetype(types));
state.EntityManager.SetComponentData(singleton, new ClientOnlyCollection
{
ProcessedPrefabs = 0,
ClientOnlyComponentTypes = m_clientOnlyComponentTypes,
BackupInfoCollection = m_clientOnlyBackupInfoCollection,
GhostTypeToPrefabMetadata = m_ghostTypeToPrefabMetadata
});
state.RequireForUpdate();
state.RequireForUpdate();
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
m_clientOnlyComponentTypes.Dispose();
m_clientOnlyBackupInfoCollection.Dispose();
m_ghostTypeToPrefabMetadata.Dispose();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
ref var clientOnlyCollection = ref SystemAPI.GetSingletonRW().ValueRW;
if(clientOnlyCollection.ClientOnlyComponentTypes.Length == 0)
return;
var ghostCollection = SystemAPI.GetSingleton();
if(!ghostCollection.IsInGame || ghostCollection.NumLoadedPrefabs == 0)
return;
if (clientOnlyCollection.ProcessedPrefabs < ghostCollection.NumLoadedPrefabs)
{
var ghostPrefabs = SystemAPI.GetSingletonBuffer(true);
for (int i = clientOnlyCollection.ProcessedPrefabs; i < ghostCollection.NumLoadedPrefabs; ++i)
{
clientOnlyCollection.ProcessGhostTypePrefab(ghostPrefabs[i].GhostType, ghostPrefabs[i].GhostPrefab, state.EntityManager);
++clientOnlyCollection.ProcessedPrefabs;
}
}
if(clientOnlyCollection.ProcessedPrefabs == 0)
return;
if (!m_destroyedGhostsWithClientOnlyBackup.IsEmpty)
{
var job = new DisposeBackupJob();
job.Run(m_destroyedGhostsWithClientOnlyBackup);
state.EntityManager.RemoveComponent(m_destroyedGhostsWithClientOnlyBackup);
}
//Add to ghost the necessary state to backup the components and buffers.
if (!m_predictedGhostsNotProcessed.IsEmpty)
{
var entities = m_predictedGhostsNotProcessed.ToEntityArray(Allocator.Temp);
var ghostTypes = m_predictedGhostsNotProcessed.ToComponentDataArray(Allocator.Temp);
//Mark entities as processed
state.EntityManager.AddComponent(m_predictedGhostsNotProcessed);
for(int ent=0;ent(Allocator.Temp);
state.EntityManager.AddComponent(m_predictedPrespawendGhostsNotProcessed);
for(int ent=0;ent();
if (networkTime.IsPartialTick)
return;
m_childEntityLookup.Update(ref state);
m_linkedEntityGroupHandle.Update(ref state);
m_backupTypeHandle.Update(ref state);
m_ghostTypeHandle.Update(ref state);
m_clientOnlyTypeHandleList.CreateOrUpdateTypeHandleList(ref state, clientOnlyCollection.ClientOnlyComponentTypes.AsArray());
var backupJob = new BackupJob
{
childEntityLookup = m_childEntityLookup,
ghostTypeHandle = m_ghostTypeHandle,
linkedEntityGroupHandle = m_linkedEntityGroupHandle,
componentTypeHandles = m_clientOnlyTypeHandleList,
clientOnlyComponentCollection = clientOnlyCollection.BackupInfoCollection,
prefabMetadata = clientOnlyCollection.GhostTypeToPrefabMetadata,
backupTypeHandle = m_backupTypeHandle,
serverTick = networkTime.ServerTick,
netDebug = SystemAPI.GetSingleton()
};
state.Dependency = backupJob.ScheduleParallel(m_predictedGhostsWithClientOnlyBackup, state.Dependency);
}
[BurstCompile]
[WithAll(typeof(ClientOnlyBackup))]
[WithNone(typeof(PredictedGhost))]
partial struct DisposeBackupJob : IJobEntity
{
public void Execute(ref ClientOnlyBackup backup)
{
backup.Dispose();
}
}
//Little class that help writing component backup. Abstract some pointer manipulation and add some
//boundary checks (in the editor)
unsafe ref struct ClientOnlyBackupWriter
{
private readonly int* backupEnableBitPtr;
private uint* backupTick;
private byte* backupCompDataPtr;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
private byte* beginCompDataPtr;
private int backupSize;
#endif
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
private void CheckBounds()
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if((backupCompDataPtr - beginCompDataPtr) >= backupSize)
throw new ArgumentOutOfRangeException();
#endif
}
public ClientOnlyBackupWriter(byte* bufPtr, int compDataOffset, int slotSize)
{
backupTick = (uint*)bufPtr;
backupEnableBitPtr = (int*)(bufPtr + sizeof(uint));
backupCompDataPtr = (bufPtr + compDataOffset);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
beginCompDataPtr = backupCompDataPtr;
backupSize = slotSize;
#endif
}
//Skip the component/buffer backup. Reset the data to 0 and advance the backup pointers
public void Skip(in ClientOnlyBackupInfo comp)
{
CheckBounds();
UnsafeUtility.MemClear(backupCompDataPtr, comp.ComponentSize);
backupCompDataPtr += comp.ComponentSize;
}
public void WriteTick(NetworkTick serverTick)
{
*backupTick = serverTick.SerializedData;
}
//Store the enable bit for the component inside the backup. The backup slot data format is
// 4 bytes 4 bytes 4 bytes
// [ tick ][EnableBits 0][EnableBits 1]
// 3130 ... 0 64 ... 32
// the bits are stored left to right
public void BackupEnableBitForComponent(int compIdx, int ent, [ReadOnly] long* bitArrayPtr)
{
int entIdx = 1 << (ent & 0x3f);
long compEnableFroEntity = ((bitArrayPtr[ent >> 6] >> entIdx) & 0x1);
int bitIdx = 1 << (compIdx & 0x1f);
backupEnableBitPtr[compIdx >> 5] &= ~bitIdx;
backupEnableBitPtr[compIdx >> 5] |= (int)(bitIdx * compEnableFroEntity);
}
//Store the component data into the backup buffer.
public void BackupComponent([ReadOnly] byte* compDataPtr, in ClientOnlyBackupInfo comp)
{
CheckBounds();
//TODO: probably better to optimise that for small component data size (like 8/64 bytes)
UnsafeUtility.MemCpy(backupCompDataPtr, compDataPtr, comp.ComponentSize);
backupCompDataPtr += comp.ComponentSize;
}
}
[BurstCompile]
unsafe struct BackupJob : IJobChunk
{
[ReadOnly] public EntityStorageInfoLookup childEntityLookup;
[ReadOnly] public ComponentTypeHandle ghostTypeHandle;
public ComponentTypeHandle backupTypeHandle;
[ReadOnly] public BufferTypeHandle linkedEntityGroupHandle;
[ReadOnly] public ClientOnlyTypeHandleList componentTypeHandles;
[ReadOnly] public NativeList clientOnlyComponentCollection;
[ReadOnly] public NativeHashMap prefabMetadata;
public NetworkTick serverTick;
public NetDebug netDebug;
public void Execute(in ArchetypeChunk chunk, int chunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
{
//Lookup for the ghost type. If the chunk contains predicted prespawed-ghost and the type has not been assigned
//yet, we skip the backup.
var ghostTypes = chunk.GetNativeArray(ref ghostTypeHandle);
var firstGhostType = ghostTypes[0];
//Retrieve the backup metadata. This should have been constructed already by the system before scheduling the jobs
if (!prefabMetadata.TryGetValue(firstGhostType, out var metadata))
{
netDebug.LogError($"Unable to find client-only backup metadata for ghost with ghost type {firstGhostType}");
return;
}
//Prepare thw writers and copy the ghost data in the backup buffer.
//Components and buffers data are backup on per entity basis, with some slightly different code in between root and child entites.
var backupWriters = stackalloc ClientOnlyBackupWriter[chunk.Count];
InitBackupWriters(chunk, metadata, backupWriters);
BackupComponents(chunk, metadata, backupWriters);
}
private void BackupComponents(in ArchetypeChunk chunk, in ClientOnlyBackupMetadata metadata, ClientOnlyBackupWriter *backupWriters)
{
int chunkEntityCount = chunk.Count;
//store the server tick for the current slot
for (int ent = 0; ent < chunkEntityCount; ++ent)
backupWriters[ent].WriteTick(serverTick);
//Backup all the root component for the whole chunk.
var iterEnd = metadata.componentBegin + metadata.numRootComponents;
var compIdx = metadata.componentBegin;
//just a convenient way to add some boundary checks
var typeHandles = new UnsafeList(componentTypeHandles.Ptr, clientOnlyComponentCollection.Length);
for (;compIdx < iterEnd; ++compIdx)
{
var comp = clientOnlyComponentCollection[compIdx];
//Buffers aren't supported
Unity.Assertions.Assert.IsFalse(comp.ComponentType.IsBuffer);
var typeHandle = typeHandles[comp.ComponentIndex];
if (!chunk.Has(ref typeHandle))
{
for (int ent = 0; ent < chunkEntityCount; ++ent)
backupWriters[ent].Skip(comp);
continue;
}
if (comp.ComponentType.IsEnableable)
{
var handle = typeHandle;
var bitArray = chunk.GetEnableableBits(ref handle);
var bitArrayPtr = (long*)UnsafeUtility.AddressOf(ref bitArray);
for (int ent = 0; ent < chunkEntityCount; ++ent)
{
backupWriters[ent].BackupEnableBitForComponent(compIdx, ent, bitArrayPtr);
}
}
var compDataPtr = (byte*)chunk
.GetDynamicComponentDataArrayReinterpret(ref typeHandle, comp.ComponentSize)
.GetUnsafeReadOnlyPtr();
for (int ent = 0; ent < chunkEntityCount; ++ent)
{
backupWriters[ent].BackupComponent(compDataPtr, comp);
compDataPtr += comp.ComponentSize;
}
}
//backup all child entities components.
if (!chunk.Has(ref linkedEntityGroupHandle))
return;
var entityGroup = chunk.GetBufferAccessor(ref linkedEntityGroupHandle);
for (int childCompIdx = compIdx; childCompIdx < metadata.componentEnd; ++childCompIdx)
{
var comp = clientOnlyComponentCollection[childCompIdx];
var typeHandle = typeHandles[comp.ComponentIndex];
for (int ent = 0; ent < chunkEntityCount; ++ent)
{
if (entityGroup[ent].Length <= comp.EntityIndex)
{
backupWriters[ent].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))
{
backupWriters[ent].Skip(comp);
continue;
}
if (comp.ComponentType.IsEnableable)
{
var handle = typeHandle;
var bitArray = childChunk.GetEnableableBits(ref handle);
var bitArrayPtr = (long*)UnsafeUtility.AddressOf(ref bitArray);
backupWriters[ent].BackupEnableBitForComponent(childCompIdx, indexInChunk, bitArrayPtr);
}
var compDataPtr = (byte*)childChunk
.GetDynamicComponentDataArrayReinterpret(ref typeHandle, comp.ComponentSize)
.GetUnsafeReadOnlyPtr();
compDataPtr += comp.ComponentSize * indexInChunk;
backupWriters[ent].BackupComponent(compDataPtr, comp);
}
}
}
private void InitBackupWriters(ArchetypeChunk chunk, in ClientOnlyBackupMetadata metadata, ClientOnlyBackupWriter* backupWriters)
{
var enableBitsIntSize = ClientOnlyBackup.EnableBitByteSize(metadata.componentEnd - metadata.componentBegin);
var compDataStartOffset = GhostComponentSerializer.SnapshotSizeAligned(sizeof(uint) + enableBitsIntSize);
//use the raw pointer for accessing the component data by ref. This is used to acquire and grow the buffer.
var states = (ClientOnlyBackup*)chunk.GetNativeArray(ref backupTypeHandle).GetUnsafePtr();
for (int ent = 0, chunkEntityCount = chunk.Count; ent < chunkEntityCount; ++ent)
{
states[ent].GrowBufferIfFull(metadata.backupSize);
var backupSlotIndex = states[ent].AcquireBackupSlot();
var slotOffset = metadata.backupSize * backupSlotIndex;
byte* backupDataPtr = states[ent].ComponentBackup.Ptr + slotOffset;
backupWriters[ent] = new ClientOnlyBackupWriter(backupDataPtr, compDataStartOffset, metadata.backupSize);
}
}
}
}
}