using System; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Entities; using Unity.Networking.Transport.Utilities; namespace Unity.NetCode.Samples { /// /// Component added to all ghosts to mark they have been pre-processed. /// public struct ClientOnlyProcessed : IComponentData { } /// /// Component added to all ghosts when there are client-only data to backup inside the prediction loop. /// internal struct ClientOnlyBackup : ICleanupComponentData, IDisposable { //Contains the raw copy of the component/buffer data present on the client //DATA LAYOUT // [tick (32bit)][enablebits (aligned 4 byte)] ... padding .. | [compdata] // compdata start at at 16byte aligned boundary (for sake of simd access) public UnsafeList ComponentBackup; /// /// The next backup slot we are writing into. /// private short m_backupWrPtr; /// /// The next backup slot we are reading from. /// private short m_backupRdPtr; /// /// The number of backup slot available in the backup buffer /// private short m_backupCapacity; /// /// The number of backup slot available in the backup buffer /// private short m_backupSlotUsed; /// /// Create and inialize the client-only backup component buffer with the specified capacity. /// /// /// public ClientOnlyBackup(int slotSize, int capacity=32) { ComponentBackup = new UnsafeList(capacity*slotSize, Allocator.Persistent); for (int i=0;i /// Relese the component backup resources /// /// public void Dispose() { ComponentBackup.Dispose(); } /// /// Return the number of uint necessary to store the client-only component enabled bits. /// /// /// private static int EnableBitIntSize(int numComponents) { return (numComponents + 31) / 32; } /// /// The size in bytes of the reserved space for /// /// /// internal static int EnableBitByteSize(int numComponents) { return sizeof(int) * EnableBitIntSize(numComponents); } public bool IsEmpty => m_backupSlotUsed == 0; public bool IsFull => m_backupSlotUsed == m_backupCapacity; public int UsedSlot => m_backupSlotUsed; public int Capacity => m_backupCapacity; public void Clear() { m_backupSlotUsed = 0; m_backupRdPtr = 0; m_backupWrPtr = 0; } public void Resize(int newCapacity, int slotSize) { if(newCapacity == m_backupCapacity) return; var oldBufferLength = ComponentBackup.Length; ComponentBackup.Resize(newCapacity*slotSize); m_backupCapacity = (short)newCapacity; //Move around the wrapped around portion to the newest allocated area such that the m_backupWrPtr > m_backupRdPtr // | w w w w w | r r r r r | n n n n n n n n n | // becomes // | - - - - - | r r r r r | w w w w n n n n n | // ^ --- new write position // This just minimize the number of memory moves required // Ideally we don't want to have another memcpy when we resize. But that is unfortunately sort of unavoidable, since // we don't have control of the UnsafeList buffer reallocation. if (m_backupWrPtr <= m_backupRdPtr) { //move data from 0 up to the backupWr pointer in front if (m_backupWrPtr > 0) { unsafe { var source = ComponentBackup.Ptr; var dest = ComponentBackup.Ptr + oldBufferLength; UnsafeUtility.MemMove(dest, source, m_backupWrPtr*slotSize); } } //always move the m_backupWrPtr in front m_backupWrPtr = (short)(m_backupRdPtr + m_backupSlotUsed); } } public void GrowBufferIfFull(int slotSize) { if (m_backupSlotUsed == m_backupCapacity) { //Grow twice as large Resize(m_backupCapacity * 2, slotSize); } } //Return a slot that can be used to write the backup. Internally advance the ring-buffer head the new position public int AcquireBackupSlot() { Assertions.Assert.IsTrue(m_backupSlotUsed < m_backupCapacity); var current = m_backupWrPtr; //Advance the backup pointer to the next position m_backupWrPtr = (short)((m_backupWrPtr + 1) % m_backupCapacity); ++m_backupSlotUsed; return current; } //Consume all backup slots that has a tick less or equal than the target tick. Reduce the consumed buffer slots and //advance the read position. public void RemoveBackupsOlderThan(NetworkTick targetTick, int backupSize) { if (m_backupSlotUsed == 0) return; unsafe { var ptr = ComponentBackup.Ptr + m_backupRdPtr*backupSize; var tick = default(NetworkTick); while (m_backupSlotUsed > 0) { tick.SerializedData = *(uint*)ptr; if (!tick.IsValid || tick.IsNewerThan(targetTick)) break; *(uint*)ptr = 0; --m_backupSlotUsed; ++m_backupRdPtr; if (m_backupRdPtr >= m_backupCapacity) { m_backupRdPtr = 0; ptr = ComponentBackup.Ptr; } else { ptr += backupSize; } } } } // return the slot for the predictionStartTick or -1 if not found public readonly int GetSlotForTick(NetworkTick predictionStartTick, int backupSize) { Assertions.Assert.IsTrue(m_backupSlotUsed > 0); unsafe { var oldestBackupPtr = ComponentBackup.Ptr + m_backupRdPtr*backupSize; var tick = new NetworkTick{SerializedData = *(uint*)oldestBackupPtr}; //If the tick are equals use this slot if (tick == predictionStartTick) return m_backupRdPtr; //It the oldest tick we have is newer, just use this one as best approximation. No older tick are present in the buffer anyway if (tick.IsNewerThan(predictionStartTick)) return m_backupRdPtr; //In normal case scenario the client is ahead of the server. //The PredictionStartTick should be usually less the current simulated tick. As such it should be in the buffer. //However, if the client is lagging a little behind (ex: initial in game connection or is trying to caching up) //it may be possible that the latest simulated tick we did is less the latest snapshot received by the server. //If for any reason the PredictionStartTick is larger than the latest backup tick we have, restoring from the backup //is not making sense, since the best approximation we have (in term of prediction) is the current state of the components var lastWrittenSlot = (m_backupWrPtr - 1) % m_backupCapacity; var latestBackupPtr = ComponentBackup.Ptr + lastWrittenSlot*backupSize; var latestTick = new NetworkTick{SerializedData = *(uint*)latestBackupPtr}; if (predictionStartTick.IsNewerThan(latestTick)) return -1; //Calculate the delta and move the backup point the desired slot. int delta = predictionStartTick.TicksSince(tick); var slotIndex = (m_backupRdPtr + delta) % m_backupCapacity; #if ENABLE_UNITY_COLLECTIONS_CHECKS tick.SerializedData = *(uint*)(ComponentBackup.Ptr + slotIndex*backupSize); //Tick should be the same Assertions.Assert.IsTrue(tick==predictionStartTick); #endif return slotIndex; } } } /// /// Store information about the component type (size, and type) and the index to retrieve the /// from the . /// internal struct ClientOnlyBackupInfo { public ComponentType ComponentType; //The size of the component inside the backup. If the component type is a buffer, it is the single element size. public int ComponentSize; //index inside the the client-only components collection. It is used to retrieve the corresponing //dynamic component type handle. public int ComponentIndex; //the entity index in the hierarchy. 0 is the root. public int EntityIndex; } /// /// The ClientOnlyBackupMetadata struct is associated to each prefab that contains client-only data, and it used to retrieve /// the ClientOnlyBackupInfo list, for a given ghost type, inside the clientOnlyBackupInfo collection. /// internal struct ClientOnlyBackupMetadata { //[start, end) range in the ClientOnlyBackupInfo collection. public int componentBegin; public int componentEnd; //num component in the root entity public int numRootComponents; //The size of the component backup (fixed). It include the tick, enabled bits and all component data. See the ClientOnlyBackup data layour public int backupSize; } }