diff --git a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
index 0087b351..f262cd55 100644
--- a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
+++ b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
@@ -171,4 +171,63 @@ public sealed class CoroutineSchedulerAdvancedTests
Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Faulted));
}
+
+ ///
+ /// 验证完成状态缓存有固定上限,避免无限增长。
+ ///
+ [Test]
+ public void CompletionStatusHistory_Should_Be_Bounded()
+ {
+ var timeSource = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(timeSource);
+ var handles = new List();
+
+ IEnumerator ImmediateCoroutine()
+ {
+ yield break;
+ }
+
+ for (var i = 0; i < 1100; i++)
+ {
+ handles.Add(scheduler.Run(ImmediateCoroutine()));
+ }
+
+ Assert.That(scheduler.TryGetCompletionStatus(handles[0], out _), Is.False);
+ Assert.That(scheduler.TryGetCompletionStatus(handles[^1], out var latestStatus), Is.True);
+ Assert.That(latestStatus, Is.EqualTo(CoroutineCompletionStatus.Completed));
+ }
+
+ ///
+ /// 验证作为首个等待指令的 WaitForCoroutine 会立即启动子协程,并沿用父协程取消令牌。
+ ///
+ [Test]
+ public async Task WaitForCoroutine_Should_Start_Child_During_Prewarm_And_Propagate_Cancellation()
+ {
+ var timeSource = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(timeSource);
+ using var cancellationTokenSource = new CancellationTokenSource();
+
+ IEnumerator ChildCoroutine()
+ {
+ yield return new Delay(10);
+ }
+
+ IEnumerator ParentCoroutine()
+ {
+ yield return new WaitForCoroutine(ChildCoroutine());
+ }
+
+ var handle = scheduler.Run(ParentCoroutine(), cancellationToken: cancellationTokenSource.Token);
+
+ Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(2));
+
+ cancellationTokenSource.Cancel();
+ timeSource.Advance(0.1);
+ scheduler.Update();
+
+ var status = await scheduler.WaitForCompletionAsync(handle);
+
+ Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled));
+ Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0));
+ }
}
\ No newline at end of file
diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs
index b11a5e57..0a24ce6d 100644
--- a/GFramework.Core/Coroutine/CoroutineScheduler.cs
+++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs
@@ -34,10 +34,13 @@ public sealed class CoroutineScheduler(
ITimeSource? realtimeTimeSource = null,
CoroutineExecutionStage executionStage = CoroutineExecutionStage.Update)
{
+ private const int CompletionStatusHistoryLimit = 1024;
+
private readonly Dictionary> _completionSources =
new();
private readonly Dictionary _completionStatuses = new();
+ private readonly Queue _completionStatusOrder = new();
private readonly Dictionary> _grouped = new();
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
private readonly Dictionary _metadata = new();
@@ -218,6 +221,7 @@ public sealed class CoroutineScheduler(
var slot = new CoroutineSlot
{
+ CancellationToken = cancellationToken,
Enumerator = coroutine,
State = CoroutineState.Running,
Handle = handle,
@@ -386,7 +390,14 @@ public sealed class CoroutineScheduler(
{
case WaitForCoroutine waitForCoroutine:
{
- var targetHandle = Run(waitForCoroutine.Coroutine);
+ var targetHandle = Run(waitForCoroutine.Coroutine, cancellationToken: slot.CancellationToken);
+ if (!targetHandle.IsValid)
+ {
+ waitForCoroutine.Complete();
+ slot.Waiting = null;
+ break;
+ }
+
slot.Waiting = waitForCoroutine;
WaitForCoroutine(slot.Handle, targetHandle);
break;
@@ -596,6 +607,8 @@ public sealed class CoroutineScheduler(
_tagged.Clear();
_grouped.Clear();
_waiting.Clear();
+ _completionStatuses.Clear();
+ _completionStatusOrder.Clear();
_nextSlot = 0;
ActiveCoroutineCount = 0;
@@ -629,7 +642,7 @@ public sealed class CoroutineScheduler(
}
else
{
- slot.Waiting = slot.Enumerator.Current;
+ HandleYieldInstruction(slot, slot.Enumerator.Current);
}
}
catch (Exception ex)
@@ -712,7 +725,7 @@ public sealed class CoroutineScheduler(
source.TrySetResult(completionStatus);
}
- _completionStatuses[handle] = completionStatus;
+ RecordCompletionStatus(handle, completionStatus);
OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
}
@@ -892,6 +905,23 @@ public sealed class CoroutineScheduler(
_statistics.PausedCount = _pausedCount;
}
+ ///
+ /// 记录协程最终状态,并对历史缓存施加固定上限,避免完成状态字典无限增长。
+ ///
+ /// 已结束的协程句柄。
+ /// 协程最终状态。
+ private void RecordCompletionStatus(CoroutineHandle handle, CoroutineCompletionStatus completionStatus)
+ {
+ _completionStatuses[handle] = completionStatus;
+ _completionStatusOrder.Enqueue(handle);
+
+ while (_completionStatusOrder.Count > CompletionStatusHistoryLimit)
+ {
+ var expiredHandle = _completionStatusOrder.Dequeue();
+ _completionStatuses.Remove(expiredHandle);
+ }
+ }
+
///
/// 为协程添加标签。
///
diff --git a/GFramework.Core/Coroutine/CoroutineSlot.cs b/GFramework.Core/Coroutine/CoroutineSlot.cs
index e4478037..1fb11994 100644
--- a/GFramework.Core/Coroutine/CoroutineSlot.cs
+++ b/GFramework.Core/Coroutine/CoroutineSlot.cs
@@ -13,6 +13,12 @@ internal sealed class CoroutineSlot
///
public CancellationTokenRegistration CancellationRegistration;
+ ///
+ /// 创建该协程时传入的取消令牌。
+ /// 当协程启动子协程时,会把同一个取消令牌继续传递下去,以保持父子协程的取消语义一致。
+ ///
+ public CancellationToken CancellationToken;
+
///
/// 协程枚举器,包含协程的执行逻辑
///
diff --git a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
index f1527dac..3f46fb1b 100644
--- a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
+++ b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using GFramework.Godot.Coroutine;
-using NUnit.Framework;
namespace GFramework.Godot.Tests.Coroutine;
@@ -49,4 +48,25 @@ public sealed class GodotTimeSourceTests
Assert.That(timeSource.DeltaTime, Is.EqualTo(0.75).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(2.0).Within(0.0001));
}
+
+ ///
+ /// 验证绝对时间源在回拨时仍保持单调,不会把 CurrentTime 拉回去。
+ ///
+ [Test]
+ public void Update_Should_Keep_Absolute_Time_Monotonic_When_Provider_Goes_Backwards()
+ {
+ var values = new Queue([5.0, 4.0, 6.5]);
+ var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true);
+
+ timeSource.Update();
+ timeSource.Update();
+
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(5.0).Within(0.0001));
+
+ timeSource.Update();
+
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(1.5).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(6.5).Within(0.0001));
+ }
}
\ No newline at end of file
diff --git a/GFramework.Godot/Coroutine/GodotTimeSource.cs b/GFramework.Godot/Coroutine/GodotTimeSource.cs
index ffdd3ae4..06cd5cbd 100644
--- a/GFramework.Godot/Coroutine/GodotTimeSource.cs
+++ b/GFramework.Godot/Coroutine/GodotTimeSource.cs
@@ -1,5 +1,4 @@
using GFramework.Core.Abstractions.Coroutine;
-using Godot;
namespace GFramework.Godot.Coroutine;
@@ -47,9 +46,11 @@ public sealed class GodotTimeSource(Func timeProvider, bool useAbsoluteT
return;
}
- DeltaTime = Math.Max(0, value - _lastAbsoluteTime);
- _lastAbsoluteTime = value;
- CurrentTime = value;
+ // 对绝对时间源做单调钳制,避免 provider 回拨后把 CurrentTime 也拉回去。
+ var nextTime = Math.Max(value, _lastAbsoluteTime);
+ DeltaTime = nextTime - _lastAbsoluteTime;
+ _lastAbsoluteTime = nextTime;
+ CurrentTime = nextTime;
return;
}
diff --git a/GFramework.Godot/Coroutine/Timing.cs b/GFramework.Godot/Coroutine/Timing.cs
index 983fdc6f..774bfcc1 100644
--- a/GFramework.Godot/Coroutine/Timing.cs
+++ b/GFramework.Godot/Coroutine/Timing.cs
@@ -1,9 +1,5 @@
using System.Reflection;
using GFramework.Core.Abstractions.Coroutine;
-using GFramework.Core.Coroutine;
-using GFramework.Core.Coroutine.Instructions;
-using GFramework.Godot.Extensions;
-using Godot;
namespace GFramework.Godot.Coroutine;
@@ -470,6 +466,11 @@ public partial class Timing : Node
return handle;
}
+ if (!GetScheduler(segment).IsCoroutineAlive(handle))
+ {
+ return handle;
+ }
+
RegisterOwnedCoroutine(owner, handle);
return handle;
}
@@ -515,7 +516,13 @@ public partial class Timing : Node
/// 被终止的协程数量。
public static int KillCoroutines(Node owner)
{
- return Instance.KillOwnedCoroutinesOnInstance(owner);
+ var count = 0;
+ foreach (var timing in EnumerateActiveInstances())
+ {
+ count += timing.KillOwnedCoroutinesOnInstance(owner);
+ }
+
+ return count;
}
///
@@ -562,7 +569,13 @@ public partial class Timing : Node
/// 该节点当前归属的活跃协程数量。
public static int GetOwnedCoroutineCount(Node owner)
{
- return Instance.GetOwnedCoroutineCountOnInstance(owner);
+ var count = 0;
+ foreach (var timing in EnumerateActiveInstances())
+ {
+ count += timing.GetOwnedCoroutineCountOnInstance(owner);
+ }
+
+ return count;
}
///
@@ -683,6 +696,15 @@ public partial class Timing : Node
return id < ActiveInstances.Length ? ActiveInstances[id] : null;
}
+ ///
+ /// 枚举所有当前已注册的 Timing 实例。
+ ///
+ /// 活跃 Timing 实例序列。
+ private static IEnumerable EnumerateActiveInstances()
+ {
+ return ActiveInstances.Where(static timing => timing is not null).Select(static timing => timing!);
+ }
+
///
/// 检查节点是否处于有效状态。
///