#if MODULE_ENTITIES using Unity.Entities; using Unity.Transforms; using UnityEngine; using Unity.Collections; using Unity.Core; using Unity.Jobs; namespace Pathfinding.ECS { [UpdateAfter(typeof(TransformSystemGroup))] public partial class AIMovementSystemGroup : ComponentSystemGroup { /// Rate manager which runs a system group multiple times if the delta time is higher than desired, but always executes the group at least once per frame public class TimeScaledRateManager : IRateManager, System.IDisposable { int numUpdatesThisFrame; int updateIndex; float stepDt; float maximumDt = 1.0f / 30.0f; NativeList cheapTimeDataQueue; NativeList timeDataQueue; double lastFullSimulation; double lastCheapSimulation; static bool cheapSimulationOnly; static bool isLastSubstep; static bool inGroup; static TimeData cheapTimeData; /// /// True if it was determined that zero substeps should be simulated. /// In this case all systems will get an opportunity to run a single update, /// but they should avoid systems that don't have to run every single frame. /// public static bool CheapSimulationOnly { get { if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); return cheapSimulationOnly; } } public static float CheapStepDeltaTime { get { if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); return cheapTimeData.DeltaTime; } } /// True when this is the last substep of the current simulation public static bool IsLastSubstep { get { if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager"); return isLastSubstep; } } public TimeScaledRateManager () { cheapTimeDataQueue = new NativeList(Allocator.Persistent); timeDataQueue = new NativeList(Allocator.Persistent); } public void Dispose () { cheapTimeDataQueue.Dispose(); timeDataQueue.Dispose(); } public bool ShouldGroupUpdate (ComponentSystemGroup group) { // if this is true, means we're being called a second or later time in a loop. if (inGroup) { group.World.PopTime(); updateIndex++; if (updateIndex >= numUpdatesThisFrame) { inGroup = false; return false; } } else { cheapTimeDataQueue.Clear(); timeDataQueue.Clear(); if (inGroup) throw new System.InvalidOperationException("Cannot nest simulation groups using TimeScaledRateManager"); var fullDt = (float)(group.World.Time.ElapsedTime - lastFullSimulation); // It has been observed that the time move backwards. // Not quite sure when it happens, but we need to guard against it. if (fullDt < 0) fullDt = 0; // If the delta time is large enough we may want to perform multiple simulation sub-steps per frame. // This is done to improve simulation stability. In particular at high time scales, but it also // helps at low fps, or if the game has a sudden long stutter. // We raise the value to a power slightly smaller than 1 to make the number of sub-steps increase // more slowly as the delta time increases. This is important to avoid the edge case when // the time it takes to run the simulation is longer than maximumDt. Otherwise the number of // simulation sub-steps would increase without bound. However, the simulation quality // may decrease a bit as the number of sub-steps increases. numUpdatesThisFrame = Mathf.FloorToInt(Mathf.Pow(fullDt / maximumDt, 0.8f)); var currentTime = group.World.Time.ElapsedTime; cheapSimulationOnly = numUpdatesThisFrame == 0; if (cheapSimulationOnly) { timeDataQueue.Add(new TimeData( lastFullSimulation, 0.0f )); cheapTimeDataQueue.Add(new TimeData( currentTime, (float)(currentTime - lastCheapSimulation) )); lastCheapSimulation = currentTime; } else { stepDt = fullDt / numUpdatesThisFrame; // Push the time for each sub-step for (int i = 0; i < numUpdatesThisFrame; i++) { var stepTime = lastFullSimulation + (i+1) * stepDt; timeDataQueue.Add(new TimeData( stepTime, stepDt )); cheapTimeDataQueue.Add(new TimeData( stepTime, (float)(stepTime - lastCheapSimulation) )); lastCheapSimulation = stepTime; } lastFullSimulation = currentTime; } numUpdatesThisFrame = Mathf.Max(1, numUpdatesThisFrame); inGroup = true; updateIndex = 0; } group.World.PushTime(timeDataQueue[updateIndex]); cheapTimeData = cheapTimeDataQueue[updateIndex]; isLastSubstep = updateIndex + 1 >= numUpdatesThisFrame; return true; } public float Timestep { get => maximumDt; set => maximumDt = value; } } protected override void OnUpdate () { // Various jobs (e.g. the JobRepairPath) in this system group may use graph data, // and they also need the graph data to be consistent during the whole update. // For example the MovementState.hierarchicalNodeIndex field needs to be valid // during the whole group update, as it may be used by the RVOSystem and FollowerControlSystem. // Locking the graph data as read-only here means that no graph updates will be performed // while these jobs are running. var readLock = AstarPath.active != null? AstarPath.active.LockGraphDataForReading() : default; // And here I thought the entities package reaching 1.0 would mean that they wouldn't just rename // properties without any compatibility code... but nope... #if MODULE_ENTITIES_1_0_8_OR_NEWER var systems = this.GetUnmanagedSystems(); for (int i = 0; i < systems.Length; i++) { ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]); state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency); } #else var systems = this.Systems; for (int i = 0; i < systems.Count; i++) { ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle); state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency); } #endif base.OnUpdate(); JobHandle readDependency = default; #if MODULE_ENTITIES_1_0_8_OR_NEWER for (int i = 0; i < systems.Length; i++) { ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]); readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency); } systems.Dispose(); #else for (int i = 0; i < systems.Count; i++) { ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle); readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency); } #endif readLock.UnlockAfter(readDependency); } protected override void OnDestroy () { base.OnDestroy(); (this.RateManager as TimeScaledRateManager).Dispose(); } protected override void OnCreate () { base.OnCreate(); this.RateManager = new TimeScaledRateManager(); } } } #endif