Skip to content

Commit eb71528

Browse files
committed
Fix for milestone removal during iteration and multiple milestones with the same value
1 parent 39d6ab1 commit eb71528

2 files changed

Lines changed: 411 additions & 227 deletions

File tree

Runtime/SimpleTimer.cs

Lines changed: 160 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ public float Duration
3838

3939
private float _duration;
4040
private bool _isRunning;
41-
private SortedList<float, TimerMilestone> _milestones = new SortedList<float, TimerMilestone>();
41+
private bool _processingMilestones;
42+
private Dictionary<Guid, TimerMilestone> _milestonesById = new Dictionary<Guid, TimerMilestone>();
43+
private SortedList<float, List<Guid>> _milestonesByTriggerValue = new SortedList<float, List<Guid>>();
4244

4345
/// <summary>
4446
/// Initializes a new instance of the SimpleTimer class with a specified duration.
@@ -94,11 +96,17 @@ public virtual void Update(float deltaTime)
9496
{
9597
if (!_isRunning) return;
9698

99+
// Store the original time remaining to check if we've reached zero
100+
var originalTimeRemaining = TimeRemaining;
101+
102+
// Update the time remaining
97103
TimeRemaining -= deltaTime;
104+
98105
CheckAndTriggerMilestones();
99106
OnTick?.Invoke(this);
100107

101-
if (!(TimeRemaining <= 0)) return;
108+
// Handle completion if we've reached zero
109+
if (!(originalTimeRemaining > 0) || !(TimeRemaining <= 0)) return;
102110

103111
_isRunning = false;
104112
TimeRemaining = 0;
@@ -107,11 +115,74 @@ public virtual void Update(float deltaTime)
107115

108116
protected virtual void CheckAndTriggerMilestones()
109117
{
110-
while (_milestones.Count > 0 && ShouldTrigger(_milestones.Values[0]))
118+
if (_processingMilestones) return;
119+
_processingMilestones = true;
120+
121+
try
122+
{
123+
// Process milestones in order of their trigger values
124+
var processedTriggerValues = new HashSet<float>();
125+
126+
while (true)
127+
{
128+
float? nextTriggerValue = null;
129+
130+
foreach (var key in _milestonesByTriggerValue.Keys)
131+
{
132+
if (processedTriggerValues.Contains(key)) continue;
133+
134+
// Get a sample milestone to check if we should process this trigger value
135+
var sampleId = _milestonesByTriggerValue[key][0];
136+
var sampleMilestone = _milestonesById[sampleId];
137+
138+
if (!ShouldTrigger(sampleMilestone)) continue;
139+
140+
if (nextTriggerValue == null || key < nextTriggerValue.Value)
141+
{
142+
nextTriggerValue = key;
143+
}
144+
}
145+
146+
if (nextTriggerValue == null) break;
147+
148+
// Mark this trigger value as processed
149+
processedTriggerValues.Add(nextTriggerValue.Value);
150+
ProcessMilestonesAtTriggerValue(nextTriggerValue.Value);
151+
}
152+
}
153+
finally
154+
{
155+
_processingMilestones = false;
156+
}
157+
}
158+
159+
private void ProcessMilestonesAtTriggerValue(float triggerValue)
160+
{
161+
if (!_milestonesByTriggerValue.TryGetValue(triggerValue, out var triggerIds) || triggerIds.Count == 0) return;
162+
163+
var milestonesToTrigger = new List<TimerMilestone>(triggerIds.Count);
164+
var idsToRemove = new List<Guid>(triggerIds);
165+
166+
// Collect valid milestones
167+
foreach (var id in idsToRemove)
168+
{
169+
if (_milestonesById.TryGetValue(id, out var milestone))
170+
{
171+
milestonesToTrigger.Add(milestone);
172+
}
173+
}
174+
175+
// Remove all milestones for this trigger value from collections
176+
_milestonesByTriggerValue.Remove(triggerValue);
177+
178+
foreach (var id in idsToRemove)
179+
{
180+
_milestonesById.Remove(id);
181+
}
182+
183+
foreach (var milestone in milestonesToTrigger)
111184
{
112-
var milestone = _milestones.Values[0];
113185
milestone.Callback?.Invoke();
114-
_milestones.RemoveAt(0);
115186
}
116187
}
117188

@@ -151,8 +222,19 @@ public virtual void ResetTimer()
151222
/// <param name="seconds">The number of seconds to fast forward.</param>
152223
public virtual void FastForward(float seconds)
153224
{
225+
if (seconds < 0) return; // Ignore negative values
226+
227+
// Store original time remaining to check for completion
228+
float originalTimeRemaining = TimeRemaining;
229+
230+
// Update time remaining
154231
TimeRemaining -= seconds;
155-
if (TimeRemaining <= 0)
232+
233+
// Check and trigger milestones based on the new time
234+
CheckAndTriggerMilestones();
235+
236+
// Handle completion if we've reached zero
237+
if (originalTimeRemaining > 0 && TimeRemaining <= 0)
156238
{
157239
TimeRemaining = 0;
158240
_isRunning = false;
@@ -170,6 +252,8 @@ public virtual void FastForward(float seconds)
170252
/// <param name="seconds">The number of seconds to rewind.</param>
171253
public virtual void Rewind(float seconds)
172254
{
255+
if (seconds < 0) return; // Ignore negative values
256+
173257
TimeRemaining += seconds;
174258
if (TimeRemaining > Duration)
175259
{
@@ -185,43 +269,101 @@ public virtual void Rewind(float seconds)
185269
/// <param name="milestone">The milestone to add to the timer.</param>
186270
public virtual void AddMilestone(TimerMilestone milestone)
187271
{
188-
_milestones.Add(milestone.TriggerValue, milestone);
272+
if (milestone == null) return;
273+
274+
// Use a combination of trigger value and a unique part to ensure key uniqueness
275+
// This prevents issues in the original code where milestones with the same trigger value
276+
// would overwrite each other in the SortedList
277+
var milestoneId = Guid.NewGuid();
278+
_milestonesById[milestoneId] = milestone;
279+
280+
if (!_milestonesByTriggerValue.TryGetValue(milestone.TriggerValue, out var milestoneList))
281+
{
282+
milestoneList = new List<Guid>();
283+
_milestonesByTriggerValue[milestone.TriggerValue] = milestoneList;
284+
}
285+
286+
milestoneList.Add(milestoneId);
189287
}
190288

191289
public virtual void RemoveMilestone(TimerMilestone milestone)
192290
{
193-
_milestones.Remove(milestone.TriggerValue);
291+
if (milestone == null) return;
292+
293+
// Find the milestone ID
294+
Guid? milestoneIdToRemove = null;
295+
foreach (var pair in _milestonesById)
296+
{
297+
if (pair.Value == milestone)
298+
{
299+
milestoneIdToRemove = pair.Key;
300+
break;
301+
}
302+
}
303+
304+
if (!milestoneIdToRemove.HasValue) return;
305+
306+
// Remove from both collections
307+
_milestonesById.Remove(milestoneIdToRemove.Value);
308+
309+
// Find and remove from trigger list
310+
if (_milestonesByTriggerValue.TryGetValue(milestone.TriggerValue, out var list))
311+
{
312+
list.Remove(milestoneIdToRemove.Value);
313+
if (list.Count == 0)
314+
{
315+
_milestonesByTriggerValue.Remove(milestone.TriggerValue);
316+
}
317+
}
194318
}
195319

196320
public virtual void RemoveMilestones(TimerMilestone[] milestones)
197321
{
198322
foreach (var milestone in milestones)
199323
{
200-
_milestones.Remove(milestone.TriggerValue);
324+
RemoveMilestone(milestone);
201325
}
202326
}
203327

204328
public virtual void RemoveAllMilestones()
205329
{
206-
_milestones.Clear();
330+
_milestonesById.Clear();
331+
_milestonesByTriggerValue.Clear();
207332
}
208333

209334
public virtual void RemoveMilestonesByCondition(Predicate<TimerMilestone> condition)
210335
{
211-
// Collect keys to remove to avoid modifying the collection during iteration
212-
var keysToRemove = new List<float>();
213-
foreach (var pair in _milestones)
336+
if (condition == null) return;
337+
338+
var milestonesToRemove = new List<Guid>();
339+
340+
// Find all milestones matching the condition
341+
foreach (var pair in _milestonesById)
214342
{
215343
if (condition(pair.Value))
216344
{
217-
keysToRemove.Add(pair.Key);
345+
milestonesToRemove.Add(pair.Key);
218346
}
219347
}
220-
221-
// Remove all identified keys
222-
foreach (var key in keysToRemove)
348+
349+
// Remove each milestone
350+
foreach (var id in milestonesToRemove)
223351
{
224-
_milestones.Remove(key);
352+
if (_milestonesById.TryGetValue(id, out var milestone))
353+
{
354+
// First remove from the trigger list
355+
if (_milestonesByTriggerValue.TryGetValue(milestone.TriggerValue, out var list))
356+
{
357+
list.Remove(id);
358+
if (list.Count == 0)
359+
{
360+
_milestonesByTriggerValue.Remove(milestone.TriggerValue);
361+
}
362+
}
363+
364+
// Then remove from the ID dictionary
365+
_milestonesById.Remove(id);
366+
}
225367
}
226368
}
227369

0 commit comments

Comments
 (0)