using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using Unity.Jobs.LowLevel.Unsafe; using Unity.Networking.Transport.Utilities; using Unity.NetCode; using Unity.Entities; [UpdateInGroup(typeof(GhostUpdateSystemGroup))] public class GameModeGhostUpdateSystem : JobComponentSystem { [BurstCompile] struct UpdateInterpolatedJob : IJobChunk { [ReadOnly] public NativeHashMap GhostMap; #if UNITY_EDITOR || DEVELOPMENT_BUILD [NativeDisableContainerSafetyRestriction] public NativeArray minMaxSnapshotTick; #pragma warning disable 649 [NativeSetThreadIndex] public int ThreadIndex; #pragma warning restore 649 #endif [ReadOnly] public ArchetypeChunkBufferType ghostSnapshotDataType; [ReadOnly] public ArchetypeChunkEntityType ghostEntityType; public ArchetypeChunkComponentType ghostGameModeDataType; public uint targetTick; public float targetTickFraction; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var deserializerState = new GhostDeserializerState { GhostMap = GhostMap }; var ghostEntityArray = chunk.GetNativeArray(ghostEntityType); var ghostSnapshotDataArray = chunk.GetBufferAccessor(ghostSnapshotDataType); var ghostGameModeDataArray = chunk.GetNativeArray(ghostGameModeDataType); #if UNITY_EDITOR || DEVELOPMENT_BUILD var minMaxOffset = ThreadIndex * (JobsUtility.CacheLineSize/4); #endif for (int entityIndex = 0; entityIndex < ghostEntityArray.Length; ++entityIndex) { var snapshot = ghostSnapshotDataArray[entityIndex]; #if UNITY_EDITOR || DEVELOPMENT_BUILD var latestTick = snapshot.GetLatestTick(); if (latestTick != 0) { if (minMaxSnapshotTick[minMaxOffset] == 0 || SequenceHelpers.IsNewer(minMaxSnapshotTick[minMaxOffset], latestTick)) minMaxSnapshotTick[minMaxOffset] = latestTick; if (minMaxSnapshotTick[minMaxOffset + 1] == 0 || SequenceHelpers.IsNewer(latestTick, minMaxSnapshotTick[minMaxOffset + 1])) minMaxSnapshotTick[minMaxOffset + 1] = latestTick; } #endif GameModeSnapshotData snapshotData; snapshot.GetDataAtTick(targetTick, targetTickFraction, out snapshotData); var ghostGameModeData = ghostGameModeDataArray[entityIndex]; ghostGameModeData.gameTimerSeconds = snapshotData.GetGameModeDatagameTimerSeconds(deserializerState); ghostGameModeData.gameTimerMessage = snapshotData.GetGameModeDatagameTimerMessage(deserializerState); ghostGameModeData.teamName0 = snapshotData.GetGameModeDatateamName0(deserializerState); ghostGameModeData.teamName1 = snapshotData.GetGameModeDatateamName1(deserializerState); ghostGameModeData.teamScore0 = snapshotData.GetGameModeDatateamScore0(deserializerState); ghostGameModeData.teamScore1 = snapshotData.GetGameModeDatateamScore1(deserializerState); ghostGameModeDataArray[entityIndex] = ghostGameModeData; } } } [BurstCompile] struct UpdatePredictedJob : IJobChunk { [ReadOnly] public NativeHashMap GhostMap; #if UNITY_EDITOR || DEVELOPMENT_BUILD [NativeDisableContainerSafetyRestriction] public NativeArray minMaxSnapshotTick; #endif #pragma warning disable 649 [NativeSetThreadIndex] public int ThreadIndex; #pragma warning restore 649 [NativeDisableParallelForRestriction] public NativeArray minPredictedTick; [ReadOnly] public ArchetypeChunkBufferType ghostSnapshotDataType; [ReadOnly] public ArchetypeChunkEntityType ghostEntityType; public ArchetypeChunkComponentType predictedGhostComponentType; public ArchetypeChunkComponentType ghostGameModeDataType; public uint targetTick; public uint lastPredictedTick; public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var deserializerState = new GhostDeserializerState { GhostMap = GhostMap }; var ghostEntityArray = chunk.GetNativeArray(ghostEntityType); var ghostSnapshotDataArray = chunk.GetBufferAccessor(ghostSnapshotDataType); var predictedGhostComponentArray = chunk.GetNativeArray(predictedGhostComponentType); var ghostGameModeDataArray = chunk.GetNativeArray(ghostGameModeDataType); #if UNITY_EDITOR || DEVELOPMENT_BUILD var minMaxOffset = ThreadIndex * (JobsUtility.CacheLineSize/4); #endif for (int entityIndex = 0; entityIndex < ghostEntityArray.Length; ++entityIndex) { var snapshot = ghostSnapshotDataArray[entityIndex]; #if UNITY_EDITOR || DEVELOPMENT_BUILD var latestTick = snapshot.GetLatestTick(); if (latestTick != 0) { if (minMaxSnapshotTick[minMaxOffset] == 0 || SequenceHelpers.IsNewer(minMaxSnapshotTick[minMaxOffset], latestTick)) minMaxSnapshotTick[minMaxOffset] = latestTick; if (minMaxSnapshotTick[minMaxOffset + 1] == 0 || SequenceHelpers.IsNewer(latestTick, minMaxSnapshotTick[minMaxOffset + 1])) minMaxSnapshotTick[minMaxOffset + 1] = latestTick; } #endif GameModeSnapshotData snapshotData; snapshot.GetDataAtTick(targetTick, out snapshotData); var predictedData = predictedGhostComponentArray[entityIndex]; var lastPredictedTickInst = lastPredictedTick; if (lastPredictedTickInst == 0 || predictedData.AppliedTick != snapshotData.Tick) lastPredictedTickInst = snapshotData.Tick; else if (!SequenceHelpers.IsNewer(lastPredictedTickInst, snapshotData.Tick)) lastPredictedTickInst = snapshotData.Tick; if (minPredictedTick[ThreadIndex] == 0 || SequenceHelpers.IsNewer(minPredictedTick[ThreadIndex], lastPredictedTickInst)) minPredictedTick[ThreadIndex] = lastPredictedTickInst; predictedGhostComponentArray[entityIndex] = new PredictedGhostComponent{AppliedTick = snapshotData.Tick, PredictionStartTick = lastPredictedTickInst}; if (lastPredictedTickInst != snapshotData.Tick) continue; var ghostGameModeData = ghostGameModeDataArray[entityIndex]; ghostGameModeData.gameTimerSeconds = snapshotData.GetGameModeDatagameTimerSeconds(deserializerState); ghostGameModeData.gameTimerMessage = snapshotData.GetGameModeDatagameTimerMessage(deserializerState); ghostGameModeData.teamName0 = snapshotData.GetGameModeDatateamName0(deserializerState); ghostGameModeData.teamName1 = snapshotData.GetGameModeDatateamName1(deserializerState); ghostGameModeData.teamScore0 = snapshotData.GetGameModeDatateamScore0(deserializerState); ghostGameModeData.teamScore1 = snapshotData.GetGameModeDatateamScore1(deserializerState); ghostGameModeDataArray[entityIndex] = ghostGameModeData; } } } private ClientSimulationSystemGroup m_ClientSimulationSystemGroup; private GhostPredictionSystemGroup m_GhostPredictionSystemGroup; private EntityQuery m_interpolatedQuery; private EntityQuery m_predictedQuery; private NativeHashMap m_ghostEntityMap; #if UNITY_EDITOR || DEVELOPMENT_BUILD private NativeArray m_ghostMinMaxSnapshotTick; #endif private GhostUpdateSystemGroup m_GhostUpdateSystemGroup; private uint m_LastPredictedTick; protected override void OnCreate() { m_GhostUpdateSystemGroup = World.GetOrCreateSystem(); m_ghostEntityMap = m_GhostUpdateSystemGroup.GhostEntityMap; #if UNITY_EDITOR || DEVELOPMENT_BUILD m_ghostMinMaxSnapshotTick = m_GhostUpdateSystemGroup.GhostSnapshotTickMinMax; #endif m_ClientSimulationSystemGroup = World.GetOrCreateSystem(); m_GhostPredictionSystemGroup = World.GetOrCreateSystem(); m_interpolatedQuery = GetEntityQuery(new EntityQueryDesc { All = new []{ ComponentType.ReadWrite(), ComponentType.ReadOnly(), ComponentType.ReadWrite(), }, None = new []{ComponentType.ReadWrite()} }); m_predictedQuery = GetEntityQuery(new EntityQueryDesc { All = new []{ ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadWrite(), } }); RequireForUpdate(GetEntityQuery(ComponentType.ReadWrite(), ComponentType.ReadOnly())); } protected override JobHandle OnUpdate(JobHandle inputDeps) { if (!m_predictedQuery.IsEmptyIgnoreFilter) { var updatePredictedJob = new UpdatePredictedJob { GhostMap = m_ghostEntityMap, #if UNITY_EDITOR || DEVELOPMENT_BUILD minMaxSnapshotTick = m_ghostMinMaxSnapshotTick, #endif minPredictedTick = m_GhostPredictionSystemGroup.OldestPredictedTick, ghostSnapshotDataType = GetArchetypeChunkBufferType(true), ghostEntityType = GetArchetypeChunkEntityType(), predictedGhostComponentType = GetArchetypeChunkComponentType(), ghostGameModeDataType = GetArchetypeChunkComponentType(), targetTick = m_ClientSimulationSystemGroup.ServerTick, lastPredictedTick = m_LastPredictedTick }; m_LastPredictedTick = m_ClientSimulationSystemGroup.ServerTick; if (m_ClientSimulationSystemGroup.ServerTickFraction < 1) m_LastPredictedTick = 0; inputDeps = updatePredictedJob.Schedule(m_predictedQuery, JobHandle.CombineDependencies(inputDeps, m_GhostUpdateSystemGroup.LastGhostMapWriter)); m_GhostPredictionSystemGroup.AddPredictedTickWriter(inputDeps); } if (!m_interpolatedQuery.IsEmptyIgnoreFilter) { var updateInterpolatedJob = new UpdateInterpolatedJob { GhostMap = m_ghostEntityMap, #if UNITY_EDITOR || DEVELOPMENT_BUILD minMaxSnapshotTick = m_ghostMinMaxSnapshotTick, #endif ghostSnapshotDataType = GetArchetypeChunkBufferType(true), ghostEntityType = GetArchetypeChunkEntityType(), ghostGameModeDataType = GetArchetypeChunkComponentType(), targetTick = m_ClientSimulationSystemGroup.InterpolationTick, targetTickFraction = m_ClientSimulationSystemGroup.InterpolationTickFraction }; inputDeps = updateInterpolatedJob.Schedule(m_interpolatedQuery, JobHandle.CombineDependencies(inputDeps, m_GhostUpdateSystemGroup.LastGhostMapWriter)); } return inputDeps; } } public partial class GameModeGhostSpawnSystem : DefaultGhostSpawnSystem { struct SetPredictedDefault : IJobParallelFor { public NativeArray predictionMask; public void Execute(int index) { predictionMask[index] = 1; } } protected override JobHandle SetPredictedGhostDefaults(NativeArray snapshots, NativeArray predictionMask, JobHandle inputDeps) { var job = new SetPredictedDefault { predictionMask = predictionMask, }; return job.Schedule(predictionMask.Length, 8, inputDeps); } }