summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs
blob: a97172cb48ea94d04c7ca42378ae1602fd2809fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#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 {
		/// <summary>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</summary>
		public class TimeScaledRateManager : IRateManager, System.IDisposable {
			int numUpdatesThisFrame;
			int updateIndex;
			float stepDt;
			float maximumDt = 1.0f / 30.0f;
			NativeList<TimeData> cheapTimeDataQueue;
			NativeList<TimeData> timeDataQueue;
			double lastFullSimulation;
			double lastCheapSimulation;
			static bool cheapSimulationOnly;
			static bool isLastSubstep;
			static bool inGroup;
			static TimeData cheapTimeData;

			/// <summary>
			/// 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.
			/// </summary>
			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;
				}
			}

			/// <summary>True when this is the last substep of the current simulation</summary>
			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<TimeData>(Allocator.Persistent);
				timeDataQueue = new NativeList<TimeData>(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