diff --git a/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs b/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs
new file mode 100644
index 00000000..a4b517ac
--- /dev/null
+++ b/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs
@@ -0,0 +1,28 @@
+namespace GFramework.Core.Abstractions.Coroutine;
+
+///
+/// 表示协程的最终完成结果。
+///
+public enum CoroutineCompletionStatus
+{
+ ///
+ /// 调度器无法确认该句柄的最终结果。
+ /// 这通常意味着句柄无效,或者句柄对应的历史结果已经不可用。
+ ///
+ Unknown,
+
+ ///
+ /// 协程自然执行结束。
+ ///
+ Completed,
+
+ ///
+ /// 协程被外部终止、清空或取消令牌中断。
+ ///
+ Cancelled,
+
+ ///
+ /// 协程在推进过程中抛出了异常。
+ ///
+ Faulted
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs b/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs
new file mode 100644
index 00000000..7b405d4c
--- /dev/null
+++ b/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs
@@ -0,0 +1,29 @@
+namespace GFramework.Core.Abstractions.Coroutine;
+
+///
+/// 表示协程调度器当前所处的执行阶段。
+///
+///
+/// 某些等待指令具有阶段语义,例如 WaitForFixedUpdate 和 WaitForEndOfFrame。
+/// 宿主应为这些语义提供匹配的调度器阶段,否则这类等待不会自然完成。
+///
+public enum CoroutineExecutionStage
+{
+ ///
+ /// 默认更新阶段。
+ /// 普通时间等待、下一帧等待以及大多数条件等待都会在该阶段推进。
+ ///
+ Update,
+
+ ///
+ /// 固定更新阶段。
+ /// 仅与固定步相关的等待指令会在该阶段完成。
+ ///
+ FixedUpdate,
+
+ ///
+ /// 帧结束阶段。
+ /// 仅与帧尾或延迟执行相关的等待指令会在该阶段完成。
+ ///
+ EndOfFrame
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
new file mode 100644
index 00000000..0087b351
--- /dev/null
+++ b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
@@ -0,0 +1,174 @@
+using GFramework.Core.Abstractions.Coroutine;
+using GFramework.Core.Coroutine;
+using GFramework.Core.Coroutine.Instructions;
+
+namespace GFramework.Core.Tests.Coroutine;
+
+///
+/// 协程调度器增强行为测试。
+///
+[TestFixture]
+public sealed class CoroutineSchedulerAdvancedTests
+{
+ ///
+ /// 验证 WaitForSecondsRealtime 使用独立的真实时间源推进。
+ ///
+ [Test]
+ public void WaitForSecondsRealtime_Should_Use_Realtime_TimeSource_When_Provided()
+ {
+ var scaledTime = new FakeTimeSource();
+ var realtimeTime = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(
+ scaledTime,
+ realtimeTimeSource: realtimeTime);
+ var completed = false;
+
+ IEnumerator Coroutine()
+ {
+ yield return new WaitForSecondsRealtime(1.0);
+ completed = true;
+ }
+
+ scheduler.Run(Coroutine());
+
+ scaledTime.Advance(0.1);
+ realtimeTime.Advance(0.4);
+ scheduler.Update();
+
+ Assert.That(completed, Is.False);
+ Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
+
+ scaledTime.Advance(0.1);
+ realtimeTime.Advance(0.6);
+ scheduler.Update();
+
+ Assert.That(completed, Is.True);
+ Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0));
+ }
+
+ ///
+ /// 验证固定更新等待指令仅在固定阶段调度器中推进。
+ ///
+ [Test]
+ public void WaitForFixedUpdate_Should_Only_Advance_On_FixedUpdate_Scheduler()
+ {
+ var defaultTime = new FakeTimeSource();
+ var fixedTime = new FakeTimeSource();
+ var defaultScheduler = new CoroutineScheduler(defaultTime);
+ var fixedScheduler = new CoroutineScheduler(
+ fixedTime,
+ executionStage: CoroutineExecutionStage.FixedUpdate);
+ var defaultCompleted = false;
+ var fixedCompleted = false;
+
+ IEnumerator DefaultCoroutine()
+ {
+ yield return new WaitForFixedUpdate();
+ defaultCompleted = true;
+ }
+
+ IEnumerator FixedCoroutine()
+ {
+ yield return new WaitForFixedUpdate();
+ fixedCompleted = true;
+ }
+
+ defaultScheduler.Run(DefaultCoroutine());
+ fixedScheduler.Run(FixedCoroutine());
+
+ defaultTime.Advance(0.1);
+ fixedTime.Advance(0.1);
+ defaultScheduler.Update();
+ fixedScheduler.Update();
+
+ Assert.That(defaultCompleted, Is.False);
+ Assert.That(defaultScheduler.ActiveCoroutineCount, Is.EqualTo(1));
+ Assert.That(fixedCompleted, Is.True);
+ Assert.That(fixedScheduler.ActiveCoroutineCount, Is.EqualTo(0));
+ }
+
+ ///
+ /// 验证取消令牌会在下一次调度循环中终止协程并记录结果。
+ ///
+ [Test]
+ public async Task CancellationToken_Should_Cancel_Coroutine_On_Next_Update()
+ {
+ var timeSource = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(timeSource);
+ using var cancellationTokenSource = new CancellationTokenSource();
+
+ IEnumerator Coroutine()
+ {
+ yield return new Delay(10);
+ }
+
+ var handle = scheduler.Run(Coroutine(), cancellationToken: cancellationTokenSource.Token);
+ cancellationTokenSource.Cancel();
+
+ timeSource.Advance(0.1);
+ scheduler.Update();
+
+ var status = await scheduler.WaitForCompletionAsync(handle);
+
+ Assert.That(scheduler.IsCoroutineAlive(handle), Is.False);
+ Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled));
+ }
+
+ ///
+ /// 验证调度器可以暴露活跃协程快照。
+ ///
+ [Test]
+ public void TryGetSnapshot_Should_Return_Current_Waiting_Instruction_And_Stage()
+ {
+ var timeSource = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(
+ timeSource,
+ executionStage: CoroutineExecutionStage.EndOfFrame);
+
+ IEnumerator Coroutine()
+ {
+ yield return new WaitForEndOfFrame();
+ }
+
+ var handle = scheduler.Run(Coroutine(), tag: "ui", group: "frame-end");
+
+ var found = scheduler.TryGetSnapshot(handle, out var snapshot);
+
+ Assert.That(found, Is.True);
+ Assert.That(snapshot.Handle, Is.EqualTo(handle));
+ Assert.That(snapshot.Tag, Is.EqualTo("ui"));
+ Assert.That(snapshot.Group, Is.EqualTo("frame-end"));
+ Assert.That(snapshot.IsWaiting, Is.True);
+ Assert.That(snapshot.WaitingInstructionType, Is.EqualTo(typeof(WaitForEndOfFrame)));
+ Assert.That(snapshot.ExecutionStage, Is.EqualTo(CoroutineExecutionStage.EndOfFrame));
+ }
+
+ ///
+ /// 验证异常结束的协程会记录为 Faulted。
+ ///
+ [Test]
+ public async Task WaitForCompletionAsync_Should_Return_Faulted_For_Failing_Coroutine()
+ {
+ var timeSource = new FakeTimeSource();
+ var scheduler = new CoroutineScheduler(timeSource);
+
+ IEnumerator Coroutine()
+ {
+ yield return new WaitOneFrame();
+ throw new InvalidOperationException("boom");
+#pragma warning disable CS0162
+ yield break;
+#pragma warning restore CS0162
+ }
+
+ var handle = scheduler.Run(Coroutine());
+ timeSource.Advance(0.1);
+ scheduler.Update();
+ timeSource.Advance(0.1);
+ scheduler.Update();
+
+ var status = await scheduler.WaitForCompletionAsync(handle);
+
+ Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Faulted));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Coroutine/CoroutineMetadata.cs b/GFramework.Core/Coroutine/CoroutineMetadata.cs
index d9d4947e..15fe0e4e 100644
--- a/GFramework.Core/Coroutine/CoroutineMetadata.cs
+++ b/GFramework.Core/Coroutine/CoroutineMetadata.cs
@@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine;
///
internal class CoroutineMetadata
{
+ ///
+ /// 协程所属调度器的执行阶段。
+ /// 该值用于诊断等待语义是否与当前宿主阶段匹配。
+ ///
+ public CoroutineExecutionStage ExecutionStage;
+
///
/// 协程的分组标识符,用于批量管理协程
///
diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs
index 4d59d22d..0aeb31f2 100644
--- a/GFramework.Core/Coroutine/CoroutineScheduler.cs
+++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Coroutine.Instructions;
@@ -6,22 +7,46 @@ using GFramework.Core.Logging;
namespace GFramework.Core.Coroutine;
///
-/// 协程调度器,用于管理和执行协程
-/// 线程安全说明:此类设计为单线程使用,所有方法应在同一线程中调用
+/// 协程调度器,用于管理和执行协程。
///
-/// 时间源接口,提供时间相关数据
-/// 实例ID,默认为1
-/// 初始容量,默认为256
-/// 是否启用统计功能,默认为false
+///
+/// 该调度器按单线程驱动模型设计,业务代码应始终在同一主线程调用其控制 API。
+/// 唯一允许跨线程进入调度器的路径是取消令牌回调;该回调只会把待终止句柄入队,
+/// 真正的清理仍然在下一次 中完成。
+///
+/// 缩放时间源,提供调度器默认推进所使用的时间数据。
+/// 协程实例编号,用于生成带宿主前缀的句柄。
+/// 调度器初始槽位容量。
+/// 是否启用协程统计功能。
+///
+/// 非缩放时间源。
+/// 若未提供,则实时等待指令会退化为使用 的时间增量。
+///
+///
+/// 当前调度器所代表的宿主阶段。
+/// 阶段型等待指令仅会在匹配的调度器阶段中完成。
+///
public sealed class CoroutineScheduler(
ITimeSource timeSource,
byte instanceId = 1,
int initialCapacity = 256,
- bool enableStatistics = false)
+ bool enableStatistics = false,
+ ITimeSource? realtimeTimeSource = null,
+ CoroutineExecutionStage executionStage = CoroutineExecutionStage.Update)
{
+ private readonly Dictionary> _completionSources =
+ new();
+
+ private readonly Dictionary _completionStatuses = new();
+ private readonly CoroutineExecutionStage _executionStage = executionStage;
private readonly Dictionary> _grouped = new();
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler));
private readonly Dictionary _metadata = new();
+ private readonly ConcurrentQueue _pendingKills = new();
+
+ private readonly ITimeSource _realtimeTimeSource = realtimeTimeSource ?? timeSource ??
+ throw new ArgumentNullException(nameof(timeSource));
+
private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null;
private readonly Dictionary> _tagged = new();
private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource));
@@ -32,59 +57,166 @@ public sealed class CoroutineScheduler(
private CoroutineSlot?[] _slots = new CoroutineSlot?[initialCapacity];
///
- /// 获取时间差值
+ /// 获取默认时间源在当前更新周期内的时间增量。
///
public double DeltaTime => _timeSource.DeltaTime;
///
- /// 获取活跃协程数量
+ /// 获取实时时间源在当前更新周期内的时间增量。
+ ///
+ public double RealtimeDeltaTime => _realtimeTimeSource.DeltaTime;
+
+ ///
+ /// 获取当前调度器代表的执行阶段。
+ ///
+ public CoroutineExecutionStage ExecutionStage => _executionStage;
+
+ ///
+ /// 获取活跃协程数量。
///
public int ActiveCoroutineCount { get; private set; }
///
- /// 获取协程统计信息(如果启用)
+ /// 获取协程统计信息。
+ /// 仅当构造时启用了统计功能时才会返回非空对象。
///
public ICoroutineStatistics? Statistics => _statistics;
///
- /// 协程异常处理回调,当协程执行过程中发生异常时触发
- /// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环
+ /// 当协程执行过程中发生异常时触发。
///
+ ///
+ /// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。
+ /// 如果调用方需要与宿主线程保持一致,请同时订阅 。
+ ///
public event Action? OnCoroutineException;
///
- /// 检查指定的协程句柄是否仍然存活
+ /// 当协程以完成、取消或失败任一结果结束时触发。
///
- /// 要检查的协程句柄
- /// 如果协程仍然存活则返回 true,否则返回 false
+ ///
+ /// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。
+ ///
+ public event Action? OnCoroutineFinished;
+
+ ///
+ /// 检查指定协程句柄是否仍然处于活跃状态。
+ ///
+ /// 要检查的协程句柄。
+ /// 如果协程仍受该调度器管理,则返回 。
public bool IsCoroutineAlive(CoroutineHandle handle)
{
- // 检查元数据字典中是否包含指定的协程句柄
return _metadata.ContainsKey(handle);
}
+ ///
+ /// 尝试获取指定句柄的当前运行快照。
+ ///
+ /// 要查询的协程句柄。
+ /// 查询成功时返回协程快照。
+ /// 如果句柄当前仍然活跃,则返回 。
+ public bool TryGetSnapshot(CoroutineHandle handle, out CoroutineSnapshot snapshot)
+ {
+ if (!_metadata.TryGetValue(handle, out var meta))
+ {
+ snapshot = default;
+ return false;
+ }
+
+ var slot = _slots[meta.SlotIndex];
+ if (slot == null)
+ {
+ snapshot = default;
+ return false;
+ }
+
+ snapshot = CreateSnapshot(meta, slot);
+ return true;
+ }
+
+ ///
+ /// 获取所有活跃协程的运行快照。
+ ///
+ /// 包含所有活跃协程的快照列表。
+ public IReadOnlyList GetActiveSnapshots()
+ {
+ var snapshots = new List(_metadata.Count);
+
+ foreach (var pair in _metadata)
+ {
+ var slot = _slots[pair.Value.SlotIndex];
+ if (slot == null)
+ {
+ continue;
+ }
+
+ snapshots.Add(CreateSnapshot(pair.Value, slot));
+ }
+
+ return snapshots;
+ }
+
+ ///
+ /// 获取指定协程的完成任务。
+ ///
+ /// 要等待完成的协程句柄。
+ /// 返回表示协程最终结果的任务。
+ ///
+ /// 如果句柄已经结束,则返回一个已完成任务。
+ /// 如果句柄无效或结果未知,则任务结果为 。
+ ///
+ public Task WaitForCompletionAsync(CoroutineHandle handle)
+ {
+ if (_completionSources.TryGetValue(handle, out var source))
+ {
+ return source.Task;
+ }
+
+ return Task.FromResult(
+ TryGetCompletionStatus(handle, out var status)
+ ? status
+ : CoroutineCompletionStatus.Unknown);
+ }
+
+ ///
+ /// 尝试读取协程的已知最终结果。
+ ///
+ /// 要查询的协程句柄。
+ /// 如果查询成功则返回最终状态。
+ /// 当调度器仍保留该句柄的完成历史时返回 。
+ public bool TryGetCompletionStatus(CoroutineHandle handle, out CoroutineCompletionStatus status)
+ {
+ return _completionStatuses.TryGetValue(handle, out status);
+ }
+
#region Run / Update
///
- /// 运行协程
+ /// 运行协程。
///
- /// 要运行的协程枚举器
- /// 协程标签,可选
- /// 协程优先级,默认为Normal
- /// 协程分组,可选
- /// 协程句柄
+ /// 要运行的协程枚举器。
+ /// 协程标签,可选。
+ /// 协程优先级,默认为 。
+ /// 协程分组,可选。
+ /// 用于取消协程的外部令牌。
+ /// 返回新创建的协程句柄;如果输入为空或取消已请求,则返回无效句柄。
public CoroutineHandle Run(
IEnumerator? coroutine,
string? tag = null,
CoroutinePriority priority = CoroutinePriority.Normal,
- string? group = null)
+ string? group = null,
+ CancellationToken cancellationToken = default)
{
- if (coroutine == null)
+ if (coroutine == null || cancellationToken.IsCancellationRequested)
+ {
return default;
+ }
if (_nextSlot >= _slots.Length)
+ {
Expand();
+ }
var handle = new CoroutineHandle(instanceId);
var slotIndex = _nextSlot++;
@@ -97,75 +229,94 @@ public sealed class CoroutineScheduler(
Priority = priority
};
+ if (cancellationToken.CanBeCanceled)
+ {
+ // 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。
+ slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle));
+ }
+
_slots[slotIndex] = slot;
_metadata[handle] = new CoroutineMetadata
{
- SlotIndex = slotIndex,
- State = CoroutineState.Running,
- Tag = tag,
- Priority = priority,
+ ExecutionStage = _executionStage,
Group = group,
- StartTime = _timeSource.CurrentTime * 1000 // 转换为毫秒
+ Priority = priority,
+ SlotIndex = slotIndex,
+ StartTime = _timeSource.CurrentTime * 1000,
+ State = CoroutineState.Running,
+ Tag = tag
};
+ _completionSources[handle] =
+ new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _completionStatuses.Remove(handle);
+
if (!string.IsNullOrEmpty(tag))
+ {
AddTag(tag, handle);
+ }
if (!string.IsNullOrEmpty(group))
+ {
AddGroup(group, handle);
+ }
_statistics?.RecordStart(priority, tag);
+ ActiveCoroutineCount++;
Prewarm(slotIndex);
- ActiveCoroutineCount++;
+ UpdateStatisticsSnapshot();
return handle;
}
///
- /// 更新所有协程状态
+ /// 推进当前调度器中的所有协程。
///
public void Update()
{
_timeSource.Update();
- var delta = _timeSource.DeltaTime;
-
- // 更新统计信息
- if (_statistics != null)
+ if (!ReferenceEquals(_realtimeTimeSource, _timeSource))
{
- _statistics.ActiveCount = ActiveCoroutineCount;
- _statistics.PausedCount = _pausedCount;
+ _realtimeTimeSource.Update();
}
- // 按优先级排序槽位索引(高优先级优先执行)
+ ProcessPendingKills();
+ UpdateStatisticsSnapshot();
+
var sortedIndices = new List(_nextSlot);
for (var i = 0; i < _nextSlot; i++)
{
var slot = _slots[i];
if (slot is { State: CoroutineState.Running })
+ {
sortedIndices.Add(i);
+ }
}
- // 按优先级降序排序
sortedIndices.Sort((a, b) =>
{
var slotA = _slots[a];
var slotB = _slots[b];
if (slotA == null || slotB == null)
+ {
return 0;
+ }
+
return slotB.Priority.CompareTo(slotA.Priority);
});
- // 遍历所有槽位并更新协程状态
foreach (var i in sortedIndices)
{
var slot = _slots[i];
if (slot is not { State: CoroutineState.Running })
+ {
continue;
+ }
try
{
- ProcessWaitingInstruction(slot, delta);
+ ProcessWaitingInstruction(slot);
if (!IsWaiting(slot))
{
@@ -177,43 +328,51 @@ public sealed class CoroutineScheduler(
OnError(i, ex);
}
}
+
+ UpdateStatisticsSnapshot();
}
///
- /// 处理协程的等待指令
+ /// 处理协程当前等待指令的推进。
///
- /// 协程槽位
- /// 时间差值
- private static void ProcessWaitingInstruction(CoroutineSlot slot, double delta)
+ /// 当前协程槽位。
+ private void ProcessWaitingInstruction(CoroutineSlot slot)
{
if (slot.Waiting == null)
+ {
return;
+ }
- slot.Waiting.Update(delta);
- if (slot.Waiting.IsDone)
- slot.Waiting = null;
+ if (!CanAdvanceInstruction(slot.Waiting))
+ {
+ return;
+ }
+
+ slot.Waiting.Update(GetInstructionDelta(slot.Waiting));
}
///
- /// 判断协程是否正在等待
+ /// 判断协程当前是否仍处于阻塞等待状态。
///
- /// 协程槽位
- /// 是否正在等待
+ /// 协程槽位。
+ /// 如果协程仍需等待则返回 。
private static bool IsWaiting(CoroutineSlot slot)
{
return slot.Waiting != null && !slot.Waiting.IsDone;
}
///
- /// 处理协程步骤推进
+ /// 推进协程到下一条等待指令,或在枚举器结束时完成协程。
///
- /// 协程槽位
- /// 槽位索引
+ /// 要推进的协程槽位。
+ /// 协程槽位索引。
private void ProcessCoroutineStep(CoroutineSlot slot, int slotIndex)
{
+ DisposeWaitingInstruction(slot);
+
if (!slot.Enumerator.MoveNext())
{
- Complete(slotIndex);
+ FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Completed);
return;
}
@@ -222,23 +381,22 @@ public sealed class CoroutineScheduler(
}
///
- /// 处理协程的yield指令
+ /// 处理协程返回的等待指令。
///
- /// 协程槽位
- /// yield指令
+ /// 当前协程槽位。
+ /// 当前等待指令。
private void HandleYieldInstruction(CoroutineSlot slot, IYieldInstruction instruction)
{
switch (instruction)
{
- // 处理 WaitForCoroutine 指令
case WaitForCoroutine waitForCoroutine:
{
- // 启动被等待的协程并建立等待关系
var targetHandle = Run(waitForCoroutine.Coroutine);
slot.Waiting = waitForCoroutine;
WaitForCoroutine(slot.Handle, targetHandle);
break;
}
+
default:
slot.Waiting = instruction;
break;
@@ -250,56 +408,69 @@ public sealed class CoroutineScheduler(
#region Pause / Resume / Kill
///
- /// 暂停指定协程
+ /// 暂停指定协程。
///
- /// 协程句柄
- /// 是否成功暂停
+ /// 协程句柄。
+ /// 如果成功暂停则返回 。
public bool Pause(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta))
+ {
return false;
+ }
var slot = _slots[meta.SlotIndex];
if (slot == null || slot.State != CoroutineState.Running)
+ {
return false;
+ }
slot.State = CoroutineState.Paused;
meta.State = CoroutineState.Paused;
_pausedCount++;
+ UpdateStatisticsSnapshot();
return true;
}
///
- /// 恢复指定协程
+ /// 恢复指定协程。
///
- /// 协程句柄
- /// 是否成功恢复
+ /// 协程句柄。
+ /// 如果成功恢复则返回 。
public bool Resume(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta))
+ {
return false;
+ }
var slot = _slots[meta.SlotIndex];
if (slot == null || slot.State != CoroutineState.Paused)
+ {
return false;
+ }
slot.State = CoroutineState.Running;
meta.State = CoroutineState.Running;
_pausedCount--;
+ UpdateStatisticsSnapshot();
return true;
}
///
- /// 终止指定协程
+ /// 终止指定协程。
///
- /// 协程句柄
- /// 是否成功终止
+ /// 协程句柄。
+ /// 如果成功终止则返回 。
public bool Kill(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta))
+ {
return false;
+ }
- Complete(meta.SlotIndex);
+ FinalizeCoroutine(meta.SlotIndex, CoroutineCompletionStatus.Cancelled);
+ UpdateStatisticsSnapshot();
return true;
}
@@ -308,50 +479,56 @@ public sealed class CoroutineScheduler(
#region Group Management
///
- /// 暂停指定分组的所有协程
+ /// 暂停指定分组的所有协程。
///
- /// 分组名称
- /// 被暂停的协程数量
+ /// 分组名称。
+ /// 实际被暂停的协程数量。
public int PauseGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
+ {
return 0;
+ }
return handles.Count(Pause);
}
///
- /// 恢复指定分组的所有协程
+ /// 恢复指定分组的所有协程。
///
- /// 分组名称
- /// 被恢复的协程数量
+ /// 分组名称。
+ /// 实际被恢复的协程数量。
public int ResumeGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
+ {
return 0;
+ }
return handles.Count(Resume);
}
///
- /// 终止指定分组的所有协程
+ /// 终止指定分组的所有协程。
///
- /// 分组名称
- /// 被终止的协程数量
+ /// 分组名称。
+ /// 实际被终止的协程数量。
public int KillGroup(string group)
{
if (!_grouped.TryGetValue(group, out var handles))
+ {
return 0;
+ }
var copy = handles.ToArray();
return copy.Count(Kill);
}
///
- /// 获取指定分组的协程数量
+ /// 获取指定分组当前包含的活跃协程数量。
///
- /// 分组名称
- /// 协程数量
+ /// 分组名称。
+ /// 分组中的活跃协程数量。
public int GetGroupCount(string group)
{
return _grouped.TryGetValue(group, out var handles) ? handles.Count : 0;
@@ -362,19 +539,21 @@ public sealed class CoroutineScheduler(
#region Wait / Tag / Clear
///
- /// 让当前协程等待目标协程完成
+ /// 让当前协程等待目标协程完成。
///
- /// 当前协程句柄
- /// 目标协程句柄
- public void WaitForCoroutine(
- CoroutineHandle current,
- CoroutineHandle target)
+ /// 当前协程句柄。
+ /// 目标协程句柄。
+ public void WaitForCoroutine(CoroutineHandle current, CoroutineHandle target)
{
if (current == target)
+ {
throw new InvalidOperationException("Coroutine cannot wait for itself.");
+ }
if (!_metadata.ContainsKey(target))
+ {
return;
+ }
if (!_waiting.TryGetValue(target, out var set))
{
@@ -386,26 +565,37 @@ public sealed class CoroutineScheduler(
}
///
- /// 根据标签终止协程
+ /// 根据标签终止协程。
///
- /// 协程标签
- /// 被终止的协程数量
+ /// 协程标签。
+ /// 被终止的协程数量。
public int KillByTag(string tag)
{
if (!_tagged.TryGetValue(tag, out var handles))
+ {
return 0;
+ }
var copy = handles.ToArray();
return copy.Count(Kill);
}
///
- /// 清空所有协程
+ /// 清空当前调度器中的所有协程。
///
- /// 被清除的协程数量
+ /// 被清理的协程数量。
public int Clear()
{
var count = ActiveCoroutineCount;
+
+ for (var i = 0; i < _nextSlot; i++)
+ {
+ if (_slots[i] != null)
+ {
+ FinalizeCoroutine(i, CoroutineCompletionStatus.Cancelled);
+ }
+ }
+
Array.Clear(_slots);
_metadata.Clear();
_tagged.Clear();
@@ -415,6 +605,7 @@ public sealed class CoroutineScheduler(
_nextSlot = 0;
ActiveCoroutineCount = 0;
_pausedCount = 0;
+ UpdateStatisticsSnapshot();
return count;
}
@@ -424,21 +615,27 @@ public sealed class CoroutineScheduler(
#region Internal
///
- /// 预热协程槽位,执行协程的第一步
+ /// 预热协程槽位,执行协程的第一步。
///
- /// 槽位索引
+ /// 槽位索引。
private void Prewarm(int slotIndex)
{
var slot = _slots[slotIndex];
if (slot == null)
+ {
return;
+ }
try
{
if (!slot.Enumerator.MoveNext())
- Complete(slotIndex);
+ {
+ FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Completed);
+ }
else
+ {
slot.Waiting = slot.Enumerator.Current;
+ }
}
catch (Exception ex)
{
@@ -447,75 +644,87 @@ public sealed class CoroutineScheduler(
}
///
- /// 完成指定槽位的协程
+ /// 按给定结果完成协程并释放相关资源。
///
- /// 槽位索引
- private void Complete(int slotIndex)
+ /// 目标槽位索引。
+ /// 最终结果。
+ /// 若协程失败,则传入对应异常。
+ private void FinalizeCoroutine(
+ int slotIndex,
+ CoroutineCompletionStatus completionStatus,
+ Exception? exception = null)
{
var slot = _slots[slotIndex];
if (slot == null)
+ {
return;
+ }
var handle = slot.Handle;
if (!handle.IsValid)
- return;
-
- // 记录统计信息
- if (_metadata.TryGetValue(handle, out var meta))
{
- var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
- _statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
+ return;
}
+ if (_metadata.TryGetValue(handle, out var meta))
+ {
+ if (meta.State == CoroutineState.Paused && _pausedCount > 0)
+ {
+ _pausedCount--;
+ }
+
+ var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime;
+ switch (completionStatus)
+ {
+ case CoroutineCompletionStatus.Completed:
+ meta.State = CoroutineState.Completed;
+ _statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag);
+ break;
+
+ case CoroutineCompletionStatus.Faulted:
+ meta.State = CoroutineState.Completed;
+ _statistics?.RecordFailure(meta.Priority, meta.Tag);
+ break;
+
+ case CoroutineCompletionStatus.Cancelled:
+ meta.State = CoroutineState.Cancelled;
+ break;
+ }
+ }
+
+ DisposeSlotResources(slot);
+
_slots[slotIndex] = null;
- ActiveCoroutineCount--;
+ if (ActiveCoroutineCount > 0)
+ {
+ ActiveCoroutineCount--;
+ }
RemoveTag(handle);
RemoveGroup(handle);
_metadata.Remove(handle);
- // 唤醒等待者
- if (!_waiting.TryGetValue(handle, out var waiters)) return;
- foreach (var waiter in waiters)
- {
- if (!_metadata.TryGetValue(waiter, out var waiterMeta)) continue;
- var s = _slots[waiterMeta.SlotIndex];
- if (s == null) continue;
- switch (s.Waiting)
- {
- // 通知 WaitForCoroutine 指令协程已完成
- case WaitForCoroutine wfc:
- wfc.Complete();
- break;
- default:
- // 其他类型的等待指令不需要特殊处理
- break;
- }
+ WakeWaiters(handle);
- s.State = CoroutineState.Running;
- waiterMeta.State = CoroutineState.Running;
+ if (_completionSources.Remove(handle, out var source))
+ {
+ source.TrySetResult(completionStatus);
}
- _waiting.Remove(handle);
+ _completionStatuses[handle] = completionStatus;
+ OnCoroutineFinished?.Invoke(handle, completionStatus, exception);
}
///
- /// 处理协程执行中的错误
+ /// 处理协程执行中的错误。
///
- /// 槽位索引
- /// 异常对象
+ /// 槽位索引。
+ /// 异常对象。
private void OnError(int slotIndex, Exception ex)
{
var slot = _slots[slotIndex];
var handle = slot?.Handle ?? default;
- // 记录统计信息
- if (handle.IsValid && _metadata.TryGetValue(handle, out var meta))
- {
- _statistics?.RecordFailure(meta.Priority, meta.Tag);
- }
-
- // 将异常回调派发到线程池,避免阻塞调度器主循环
var handler = OnCoroutineException;
if (handler != null)
{
@@ -527,21 +736,141 @@ public sealed class CoroutineScheduler(
}
catch (Exception callbackEx)
{
- // 防止回调异常传播,记录到控制台
_logger.Error($"[CoroutineScheduler] Exception in error callback: {callbackEx}");
}
});
}
- // 输出到控制台作为后备
_logger.Error($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}");
-
- // 完成协程
- Complete(slotIndex);
+ FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Faulted, ex);
}
///
- /// 扩展协程槽位数组容量
+ /// 判断指定等待指令是否允许在当前调度器阶段中推进。
+ ///
+ /// 要检查的等待指令。
+ /// 如果当前阶段允许推进该等待指令,则返回 。
+ private bool CanAdvanceInstruction(IYieldInstruction instruction)
+ {
+ return instruction switch
+ {
+ WaitForFixedUpdate => _executionStage == CoroutineExecutionStage.FixedUpdate,
+ WaitForEndOfFrame => _executionStage == CoroutineExecutionStage.EndOfFrame,
+ _ => true
+ };
+ }
+
+ ///
+ /// 为指定等待指令选择合适的时间增量。
+ ///
+ /// 待推进的等待指令。
+ /// 与等待语义匹配的时间增量。
+ private double GetInstructionDelta(IYieldInstruction instruction)
+ {
+ return instruction switch
+ {
+ WaitForSecondsRealtime => RealtimeDeltaTime,
+ _ => DeltaTime
+ };
+ }
+
+ ///
+ /// 处理跨线程入队的待终止协程。
+ ///
+ private void ProcessPendingKills()
+ {
+ while (_pendingKills.TryDequeue(out var handle))
+ {
+ Kill(handle);
+ }
+ }
+
+ ///
+ /// 释放单个槽位持有的资源。
+ ///
+ /// 待释放的槽位。
+ private static void DisposeSlotResources(CoroutineSlot slot)
+ {
+ DisposeWaitingInstruction(slot);
+ slot.CancellationRegistration.Dispose();
+ slot.Enumerator.Dispose();
+ }
+
+ ///
+ /// 如果当前等待指令实现了可释放接口,则在协程继续前先释放该指令。
+ ///
+ /// 当前协程槽位。
+ private static void DisposeWaitingInstruction(CoroutineSlot slot)
+ {
+ if (slot.Waiting is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ slot.Waiting = null;
+ }
+
+ ///
+ /// 唤醒所有等待目标协程完成的协程。
+ ///
+ /// 已结束的目标协程句柄。
+ private void WakeWaiters(CoroutineHandle handle)
+ {
+ if (!_waiting.TryGetValue(handle, out var waiters))
+ {
+ return;
+ }
+
+ foreach (var waiter in waiters)
+ {
+ if (!_metadata.TryGetValue(waiter, out var waiterMeta))
+ {
+ continue;
+ }
+
+ var waiterSlot = _slots[waiterMeta.SlotIndex];
+ if (waiterSlot == null)
+ {
+ continue;
+ }
+
+ if (waiterSlot.Waiting is WaitForCoroutine waitForCoroutine)
+ {
+ waitForCoroutine.Complete();
+ }
+
+ if (waiterSlot.State != CoroutineState.Paused)
+ {
+ waiterSlot.State = CoroutineState.Running;
+ waiterMeta.State = CoroutineState.Running;
+ }
+ }
+
+ _waiting.Remove(handle);
+ }
+
+ ///
+ /// 创建协程快照。
+ ///
+ /// 协程元数据。
+ /// 协程槽位。
+ /// 与当前槽位一致的只读快照。
+ private CoroutineSnapshot CreateSnapshot(CoroutineMetadata metadata, CoroutineSlot slot)
+ {
+ return new CoroutineSnapshot(
+ slot.Handle,
+ metadata.State,
+ metadata.Priority,
+ metadata.Tag,
+ metadata.Group,
+ metadata.StartTime,
+ IsWaiting(slot),
+ slot.Waiting?.GetType(),
+ metadata.ExecutionStage);
+ }
+
+ ///
+ /// 扩展协程槽位数组容量。
///
private void Expand()
{
@@ -549,10 +878,24 @@ public sealed class CoroutineScheduler(
}
///
- /// 为协程添加标签
+ /// 更新统计对象中的活动快照数据。
///
- /// 标签名称
- /// 协程句柄
+ private void UpdateStatisticsSnapshot()
+ {
+ if (_statistics == null)
+ {
+ return;
+ }
+
+ _statistics.ActiveCount = ActiveCoroutineCount;
+ _statistics.PausedCount = _pausedCount;
+ }
+
+ ///
+ /// 为协程添加标签。
+ ///
+ /// 标签名称。
+ /// 协程句柄。
private void AddTag(string tag, CoroutineHandle handle)
{
if (!_tagged.TryGetValue(tag, out var set))
@@ -566,29 +909,33 @@ public sealed class CoroutineScheduler(
}
///
- /// 移除协程标签
+ /// 移除协程标签。
///
- /// 协程句柄
+ /// 协程句柄。
private void RemoveTag(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta) || meta.Tag == null)
+ {
return;
+ }
if (_tagged.TryGetValue(meta.Tag, out var set))
{
set.Remove(handle);
if (set.Count == 0)
+ {
_tagged.Remove(meta.Tag);
+ }
}
meta.Tag = null;
}
///
- /// 为协程添加分组
+ /// 为协程添加分组。
///
- /// 分组名称
- /// 协程句柄
+ /// 分组名称。
+ /// 协程句柄。
private void AddGroup(string group, CoroutineHandle handle)
{
if (!_grouped.TryGetValue(group, out var set))
@@ -602,19 +949,23 @@ public sealed class CoroutineScheduler(
}
///
- /// 移除协程分组
+ /// 移除协程分组。
///
- /// 协程句柄
+ /// 协程句柄。
private void RemoveGroup(CoroutineHandle handle)
{
if (!_metadata.TryGetValue(handle, out var meta) || meta.Group == null)
+ {
return;
+ }
if (_grouped.TryGetValue(meta.Group, out var set))
{
set.Remove(handle);
if (set.Count == 0)
+ {
_grouped.Remove(meta.Group);
+ }
}
meta.Group = null;
diff --git a/GFramework.Core/Coroutine/CoroutineSlot.cs b/GFramework.Core/Coroutine/CoroutineSlot.cs
index 7821e486..e4478037 100644
--- a/GFramework.Core/Coroutine/CoroutineSlot.cs
+++ b/GFramework.Core/Coroutine/CoroutineSlot.cs
@@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine;
///
internal sealed class CoroutineSlot
{
+ ///
+ /// 由外部取消令牌创建的注册。
+ /// 调度器在协程结束时必须释放该注册,避免泄漏取消回调。
+ ///
+ public CancellationTokenRegistration CancellationRegistration;
+
///
/// 协程枚举器,包含协程的执行逻辑
///
diff --git a/GFramework.Core/Coroutine/CoroutineSnapshot.cs b/GFramework.Core/Coroutine/CoroutineSnapshot.cs
new file mode 100644
index 00000000..c96b77bd
--- /dev/null
+++ b/GFramework.Core/Coroutine/CoroutineSnapshot.cs
@@ -0,0 +1,29 @@
+using GFramework.Core.Abstractions.Coroutine;
+
+namespace GFramework.Core.Coroutine;
+
+///
+/// 表示某个活跃协程在调度器中的只读运行快照。
+///
+/// 协程句柄。
+/// 当前协程状态。
+/// 当前协程优先级。
+/// 可选标签。
+/// 可选分组。
+/// 协程启动时间,单位为毫秒。
+/// 当前是否正被等待指令阻塞。
+///
+/// 当前等待指令的具体类型。
+/// 若协程当前未处于等待状态,则该值为 。
+///
+/// 所属调度器的执行阶段。
+public readonly record struct CoroutineSnapshot(
+ CoroutineHandle Handle,
+ CoroutineState State,
+ CoroutinePriority Priority,
+ string? Tag,
+ string? Group,
+ double StartTimeMs,
+ bool IsWaiting,
+ Type? WaitingInstructionType,
+ CoroutineExecutionStage ExecutionStage);
\ No newline at end of file
diff --git a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
new file mode 100644
index 00000000..f1527dac
--- /dev/null
+++ b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using GFramework.Godot.Coroutine;
+using NUnit.Framework;
+
+namespace GFramework.Godot.Tests.Coroutine;
+
+///
+/// GodotTimeSource 的单元测试。
+///
+[TestFixture]
+public sealed class GodotTimeSourceTests
+{
+ ///
+ /// 验证增量模式会直接累加传入的 delta。
+ ///
+ [Test]
+ public void Update_Should_Accumulate_Delta_When_Using_Delta_Mode()
+ {
+ var values = new Queue([0.1, 0.2]);
+ var timeSource = new GodotTimeSource(() => values.Dequeue());
+
+ timeSource.Update();
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0.1).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(0.1).Within(0.0001));
+
+ timeSource.Update();
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0.2).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(0.3).Within(0.0001));
+ }
+
+ ///
+ /// 验证绝对时间模式会根据前后两次采样计算 delta。
+ ///
+ [Test]
+ public void Update_Should_Calculate_Delta_When_Using_Absolute_Time_Mode()
+ {
+ var values = new Queue([1.0, 1.25, 2.0]);
+ var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true);
+
+ timeSource.Update();
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(1.0).Within(0.0001));
+
+ timeSource.Update();
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0.25).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(1.25).Within(0.0001));
+
+ timeSource.Update();
+ Assert.That(timeSource.DeltaTime, Is.EqualTo(0.75).Within(0.0001));
+ Assert.That(timeSource.CurrentTime, Is.EqualTo(2.0).Within(0.0001));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
new file mode 100644
index 00000000..62e41903
--- /dev/null
+++ b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ $(TestTargetFrameworks)
+ disable
+ enable
+ false
+ true
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs b/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs
index d233ad17..48ca3913 100644
--- a/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs
+++ b/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs
@@ -35,6 +35,25 @@ public static class CoroutineNodeExtensions
return Timing.RunCoroutine(coroutine, segment, tag);
}
+ ///
+ /// 以指定节点作为生命周期所有者运行协程。
+ ///
+ /// 拥有该协程生命周期的节点。
+ /// 要启动的协程枚举器。
+ /// 协程运行的时间段。
+ /// 协程标签。
+ /// 可选取消令牌。
+ /// 返回协程句柄。
+ public static CoroutineHandle RunCoroutine(
+ this Node owner,
+ IEnumerator coroutine,
+ Segment segment = Segment.Process,
+ string? tag = null,
+ CancellationToken cancellationToken = default)
+ {
+ return Timing.RunOwnedCoroutine(owner, coroutine, segment, tag, cancellationToken);
+ }
+
///
/// 让协程在指定节点被销毁时自动取消。
///
diff --git a/GFramework.Godot/Coroutine/GodotTimeSource.cs b/GFramework.Godot/Coroutine/GodotTimeSource.cs
index b33bc3d7..ffdd3ae4 100644
--- a/GFramework.Godot/Coroutine/GodotTimeSource.cs
+++ b/GFramework.Godot/Coroutine/GodotTimeSource.cs
@@ -1,42 +1,81 @@
using GFramework.Core.Abstractions.Coroutine;
+using Godot;
namespace GFramework.Godot.Coroutine;
///
-/// Godot时间源实现,用于提供基于Godot引擎的时间信息
+/// Godot 时间源实现,用于为协程调度器提供缩放时间或真实时间数据。
///
-/// 获取增量时间的函数委托
-public class GodotTimeSource(Func getDeltaFunc) : ITimeSource
+///
+/// 时间提供函数。
+/// 在默认模式下该函数返回“本帧增量”;在绝对时间模式下该函数返回“当前绝对时间(秒)”。
+///
+///
+/// 是否把 返回值解释为绝对时间。
+/// 启用后, 会通过相邻两次读数计算 。
+///
+public sealed class GodotTimeSource(Func timeProvider, bool useAbsoluteTime = false) : ITimeSource
{
- private readonly Func _getDeltaFunc = getDeltaFunc ?? throw new ArgumentNullException(nameof(getDeltaFunc));
+ private readonly Func _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
+ private bool _initialized;
+ private double _lastAbsoluteTime;
///
- /// 获取当前累计时间
+ /// 获取当前累计时间。
///
public double CurrentTime { get; private set; }
///
- /// 获取上一帧的时间增量
+ /// 获取上一帧的时间增量。
///
public double DeltaTime { get; private set; }
///
- /// 更新时间源,计算新的增量时间和累计时间
+ /// 更新时间源,计算新的时间增量与累计时间。
///
public void Update()
{
- // 调用外部提供的函数获取当前帧的时间增量
- DeltaTime = _getDeltaFunc();
- // 累加到总时间中
+ var value = _timeProvider();
+ if (useAbsoluteTime)
+ {
+ if (!_initialized)
+ {
+ _initialized = true;
+ _lastAbsoluteTime = value;
+ CurrentTime = value;
+ DeltaTime = 0;
+ return;
+ }
+
+ DeltaTime = Math.Max(0, value - _lastAbsoluteTime);
+ _lastAbsoluteTime = value;
+ CurrentTime = value;
+ return;
+ }
+
+ DeltaTime = value;
CurrentTime += DeltaTime;
}
///
- /// 重置时间源到初始状态
+ /// 创建基于 Godot 单调时钟的真实时间源。
+ ///
+ /// 返回一个不受场景暂停与时间缩放影响的时间源实例。
+ public static GodotTimeSource CreateRealtime()
+ {
+ return new GodotTimeSource(
+ () => Time.GetTicksUsec() / 1_000_000.0,
+ useAbsoluteTime: true);
+ }
+
+ ///
+ /// 重置时间源到初始状态。
///
public void Reset()
{
CurrentTime = 0;
DeltaTime = 0;
+ _initialized = false;
+ _lastAbsoluteTime = 0;
}
}
\ No newline at end of file
diff --git a/GFramework.Godot/Coroutine/Timing.cs b/GFramework.Godot/Coroutine/Timing.cs
index 881c36be..983fdc6f 100644
--- a/GFramework.Godot/Coroutine/Timing.cs
+++ b/GFramework.Godot/Coroutine/Timing.cs
@@ -8,68 +8,74 @@ using Godot;
namespace GFramework.Godot.Coroutine;
///
-/// Godot协程管理器,提供基于不同更新循环的协程调度功能
-/// 支持Process、PhysicsProcess和DeferredProcess三种执行段的协程管理
+/// Godot 协程管理器,负责在不同的引擎更新阶段驱动 Core 协程调度器。
///
+///
+/// 该类型为 Godot 层协程功能的统一入口。
+/// 它不仅提供静态运行 API,也负责把 Godot 的 Process、Physics 与 Deferred 生命周期映射为
+/// ,以保证阶段型等待指令的语义真实有效。
+///
public partial class Timing : Node
{
- private static Timing? _instance;
+ private const string NotInitializedMessage = "Timing not yet initialized (_Ready not executed)";
+
private static readonly Timing?[] ActiveInstances = new Timing?[16];
+ private static Timing? _instance;
+ private readonly Dictionary _ownedCoroutineRegistrations = new();
+ private readonly Dictionary> _ownedCoroutinesByNode = new();
+ private GodotTimeSource? _deferredRealtimeTimeSource;
private CoroutineScheduler? _deferredScheduler;
private GodotTimeSource? _deferredTimeSource;
private ushort _frameCounter;
-
private byte _instanceId = 1;
+ private GodotTimeSource? _physicsRealtimeTimeSource;
private CoroutineScheduler? _physicsScheduler;
private GodotTimeSource? _physicsTimeSource;
-
- private CoroutineScheduler? _processScheduler;
-
- private GodotTimeSource? _processTimeSource;
+ private GodotTimeSource? _processIgnorePauseRealtimeTimeSource;
private CoroutineScheduler? _processIgnorePauseScheduler;
private GodotTimeSource? _processIgnorePauseTimeSource;
- private const string NotInitializedMessage = "Timing not yet initialized (_Ready not executed)";
+ private GodotTimeSource? _processRealtimeTimeSource;
+ private CoroutineScheduler? _processScheduler;
+ private GodotTimeSource? _processTimeSource;
///
- /// 获取Process调度器,如果未初始化则抛出异常
+ /// 获取 Process 调度器。
///
private CoroutineScheduler ProcessScheduler =>
- _processScheduler ?? throw new InvalidOperationException(
- NotInitializedMessage);
+ _processScheduler ?? throw new InvalidOperationException(NotInitializedMessage);
///
- /// 获取忽略暂停的Process调度器,如果未初始化则抛出异常
+ /// 获取忽略暂停的 Process 调度器。
///
private CoroutineScheduler ProcessIgnorePauseScheduler =>
- _processIgnorePauseScheduler ?? throw new InvalidOperationException(
- NotInitializedMessage);
+ _processIgnorePauseScheduler ?? throw new InvalidOperationException(NotInitializedMessage);
///
- /// 获取Physics调度器,如果未初始化则抛出异常
+ /// 获取 Physics 调度器。
///
private CoroutineScheduler PhysicsScheduler =>
- _physicsScheduler ?? throw new InvalidOperationException(
- NotInitializedMessage);
+ _physicsScheduler ?? throw new InvalidOperationException(NotInitializedMessage);
///
- /// 获取Deferred调度器,如果未初始化则抛出异常
+ /// 获取 Deferred 调度器。
///
private CoroutineScheduler DeferredScheduler =>
- _deferredScheduler ?? throw new InvalidOperationException(
- NotInitializedMessage);
+ _deferredScheduler ?? throw new InvalidOperationException(NotInitializedMessage);
#region 单例
///
- /// 获取Timing单例实例
- /// 如果实例不存在则自动创建并添加到场景树根节点
+ /// 获取 Timing 单例实例。
+ /// 如果实例不存在,则会自动创建并挂载到场景树根节点。
///
public static Timing Instance
{
get
{
if (_instance != null)
+ {
return _instance;
+ }
var tree = (SceneTree)Engine.GetMainLoop();
_instance = tree.Root.GetNodeOrNull(nameof(Timing));
@@ -83,27 +89,70 @@ public partial class Timing : Node
Name = nameof(Timing)
};
tree.Root.WaitUntilReady(() => tree.Root.AddChild(_instance));
-
return _instance;
}
}
#endregion
+ private sealed class OwnedCoroutineRegistration
+ {
+ ///
+ /// 创建一个节点归属协程注册记录。
+ ///
+ /// 归属节点。
+ /// 归属节点的 Godot 实例 ID。
+ /// 被管理的协程句柄。
+ /// 节点退出场景树时触发的终止逻辑。
+ public OwnedCoroutineRegistration(Node owner, ulong ownerId, CoroutineHandle handle,
+ Action killCallback)
+ {
+ Handle = handle;
+ Owner = new WeakReference(owner);
+ OwnerId = ownerId;
+ OnOwnerTreeExiting = () => killCallback(handle);
+ }
+
+ ///
+ /// 获取协程句柄。
+ ///
+ public CoroutineHandle Handle { get; }
+
+ ///
+ /// 获取节点弱引用。
+ ///
+ public WeakReference Owner { get; }
+
+ ///
+ /// 获取归属节点 ID。
+ ///
+ public ulong OwnerId { get; }
+
+ ///
+ /// 获取节点退出场景树时使用的清理回调。
+ ///
+ public Action OnOwnerTreeExiting { get; }
+ }
+
#region Debug 信息
///
- /// 获取Process段活跃协程数量
+ /// 获取 Process 段活跃协程数量。
///
public int ProcessCoroutines => _processScheduler?.ActiveCoroutineCount ?? 0;
///
- /// 获取Physics段活跃协程数量
+ /// 获取忽略暂停的 Process 段活跃协程数量。
+ ///
+ public int ProcessIgnorePauseCoroutines => _processIgnorePauseScheduler?.ActiveCoroutineCount ?? 0;
+
+ ///
+ /// 获取 Physics 段活跃协程数量。
///
public int PhysicsCoroutines => _physicsScheduler?.ActiveCoroutineCount ?? 0;
///
- /// 获取Deferred段活跃协程数量
+ /// 获取 Deferred 段活跃协程数量。
///
public int DeferredCoroutines => _deferredScheduler?.ActiveCoroutineCount ?? 0;
@@ -112,34 +161,36 @@ public partial class Timing : Node
#region 生命周期
///
- /// 节点就绪时的初始化方法
- /// 设置处理优先级,初始化调度器,并注册实例
+ /// 节点就绪时初始化所有调度器与生命周期桥接。
///
public override void _Ready()
{
ProcessPriority = -1;
ProcessMode = ProcessModeEnum.Always;
- TrySetPhysicsPriority(-1);
-
- InitializeSchedulers();
RegisterInstance();
+ TrySetPhysicsPriority(-1);
+ InitializeSchedulers();
}
///
- /// 节点退出场景树时的清理方法
- /// 从活动实例数组中移除当前实例并清理必要资源
+ /// 节点退出场景树时清理实例与归属关系。
///
public override void _ExitTree()
{
+ DetachAllOwnedRegistrations();
+ ClearOnInstance();
+
if (_instanceId < ActiveInstances.Length)
+ {
ActiveInstances[_instanceId] = null;
+ }
CleanupInstanceIfNecessary();
}
///
- /// 清理实例引用
+ /// 清理单例引用。
///
private static void CleanupInstanceIfNecessary()
{
@@ -147,41 +198,41 @@ public partial class Timing : Node
}
///
- /// 每帧处理逻辑
- /// 更新Process调度器,增加帧计数器,并安排延迟处理
+ /// Godot 每帧更新逻辑。
///
- /// 时间增量
+ /// 本帧 Process 增量。
public override void _Process(double delta)
{
var paused = GetTree().Paused;
if (!paused)
+ {
_processScheduler?.Update();
+ }
_processIgnorePauseScheduler?.Update();
_frameCounter++;
-
CallDeferred(nameof(ProcessDeferred));
}
///
- /// 物理处理逻辑
- /// 更新Physics调度器
+ /// Godot 物理帧更新逻辑。
///
- /// 物理时间增量
+ /// 本帧 Physics 增量。
public override void _PhysicsProcess(double delta)
{
_physicsScheduler?.Update();
}
///
- /// 延迟处理逻辑
- /// 更新Deferred调度器
+ /// 当前帧尾的延迟更新逻辑。
///
private void ProcessDeferred()
{
if (GetTree().Paused)
+ {
return;
+ }
_deferredScheduler?.Update();
}
@@ -189,54 +240,78 @@ public partial class Timing : Node
#endregion
#region 初始化
+
///
- /// 预热函数,用于确保实例已初始化。
- /// 此函数通过访问 Instance 属性来触发可能的延迟初始化逻辑,
- /// 从而避免在首次使用时产生性能开销。
+ /// 预热 Timing 单例,以便在业务逻辑首次使用前完成挂载。
///
public static void Prewarm()
{
- // 访问 Instance 属性以触发初始化逻辑
_ = Instance;
}
+
///
- /// 初始化所有调度器和时间源
- /// 创建Process、Physics和Deferred三个调度器实例
+ /// 初始化所有调度器和时间源。
///
private void InitializeSchedulers()
{
_processTimeSource = new GodotTimeSource(GetProcessDeltaTime);
+ _processRealtimeTimeSource = GodotTimeSource.CreateRealtime();
_processIgnorePauseTimeSource = new GodotTimeSource(GetProcessDeltaTime);
+ _processIgnorePauseRealtimeTimeSource = GodotTimeSource.CreateRealtime();
_physicsTimeSource = new GodotTimeSource(GetPhysicsProcessDeltaTime);
+ _physicsRealtimeTimeSource = GodotTimeSource.CreateRealtime();
_deferredTimeSource = new GodotTimeSource(GetProcessDeltaTime);
+ _deferredRealtimeTimeSource = GodotTimeSource.CreateRealtime();
_processScheduler = new CoroutineScheduler(
_processTimeSource,
- _instanceId
- );
+ _instanceId,
+ 256,
+ false,
+ _processRealtimeTimeSource,
+ CoroutineExecutionStage.Update);
_processIgnorePauseScheduler = new CoroutineScheduler(
_processIgnorePauseTimeSource,
- _instanceId
- );
+ _instanceId,
+ 256,
+ false,
+ _processIgnorePauseRealtimeTimeSource,
+ CoroutineExecutionStage.Update);
_physicsScheduler = new CoroutineScheduler(
_physicsTimeSource,
_instanceId,
- 128
- );
+ 128,
+ false,
+ _physicsRealtimeTimeSource,
+ CoroutineExecutionStage.FixedUpdate);
_deferredScheduler = new CoroutineScheduler(
_deferredTimeSource,
_instanceId,
- 64
- );
+ 64,
+ false,
+ _deferredRealtimeTimeSource,
+ CoroutineExecutionStage.EndOfFrame);
+ AttachSchedulerLifecycleHandlers(ProcessScheduler);
+ AttachSchedulerLifecycleHandlers(ProcessIgnorePauseScheduler);
+ AttachSchedulerLifecycleHandlers(PhysicsScheduler);
+ AttachSchedulerLifecycleHandlers(DeferredScheduler);
}
///
- /// 注册当前实例到活动实例数组中
- /// 如果当前ID已被占用则寻找可用ID
+ /// 把调度器的完成通知接入 Timing 的节点归属清理逻辑。
+ ///
+ /// 待桥接的调度器。
+ private void AttachSchedulerLifecycleHandlers(CoroutineScheduler scheduler)
+ {
+ scheduler.OnCoroutineFinished += HandleCoroutineFinished;
+ }
+
+ ///
+ /// 注册当前 Timing 实例到实例槽位表中。
///
private void RegisterInstance()
{
@@ -247,31 +322,31 @@ public partial class Timing : Node
}
for (byte i = 1; i < ActiveInstances.Length; i++)
+ {
if (ActiveInstances[i] == null)
{
_instanceId = i;
ActiveInstances[i] = this;
return;
}
+ }
throw new OverflowException("最多只能存在 15 个 Timing 实例");
}
///
- /// 尝试设置物理处理优先级
- /// 使用反射方式设置ProcessPhysicsPriority属性
+ /// 通过反射设置 Physics 处理优先级,兼容不同 Godot 版本的 API 表面。
///
- /// 物理处理优先级
- private static void TrySetPhysicsPriority(int priority)
+ /// 要设置的优先级。
+ private void TrySetPhysicsPriority(int priority)
{
try
{
typeof(Node)
.GetProperty(
"ProcessPhysicsPriority",
- BindingFlags.Instance |
- BindingFlags.Public)
- ?.SetValue(Instance, priority);
+ BindingFlags.Instance | BindingFlags.Public)
+ ?.SetValue(this, priority);
}
catch
{
@@ -284,70 +359,119 @@ public partial class Timing : Node
#region 协程启动 API
///
- /// 运行游戏级协程(受暂停影响)
+ /// 运行受场景暂停影响的游戏级协程。
///
- /// 要运行的协程枚举器
- /// 协程标签,用于批量操作
- /// 协程句柄
- public static CoroutineHandle RunGameCoroutine(
- IEnumerator coroutine,
- string? tag = null)
+ /// 要运行的协程枚举器。
+ /// 协程标签。
+ /// 新创建的协程句柄。
+ public static CoroutineHandle RunGameCoroutine(IEnumerator coroutine, string? tag = null)
{
return RunCoroutine(coroutine, Segment.Process, tag);
}
///
- /// 运行UI级协程(忽略暂停)
+ /// 运行忽略场景暂停的 UI 级协程。
///
- /// 要运行的协程枚举器
- /// 协程标签,用于批量操作
- /// 协程句柄
- public static CoroutineHandle RunUiCoroutine(
- IEnumerator coroutine,
- string? tag = null)
+ /// 要运行的协程枚举器。
+ /// 协程标签。
+ /// 新创建的协程句柄。
+ public static CoroutineHandle RunUiCoroutine(IEnumerator coroutine, string? tag = null)
{
return RunCoroutine(coroutine, Segment.ProcessIgnorePause, tag);
}
///
- /// 在指定段运行协程
+ /// 在指定段运行协程。
///
- /// 要运行的协程枚举器
- /// 协程执行段(Process/PhysicsProcess/DeferredProcess)
- /// 协程标签,用于批量操作
- /// 协程句柄
+ /// 要运行的协程枚举器。
+ /// 协程执行段。
+ /// 协程标签。
+ /// 可选取消令牌。
+ /// 新创建的协程句柄。
public static CoroutineHandle RunCoroutine(
IEnumerator coroutine,
Segment segment = Segment.Process,
- string? tag = null)
+ string? tag = null,
+ CancellationToken cancellationToken = default)
{
- return Instance.RunCoroutineOnInstance(coroutine, segment, tag);
+ return Instance.RunCoroutineOnInstance(coroutine, segment, tag, cancellationToken);
}
///
- /// 在当前实例上运行协程
- /// 根据指定的段选择对应的调度器运行协程
+ /// 运行一个显式归属于指定节点的协程。
///
- /// 要运行的协程枚举器
- /// 协程执行段
- /// 协程标签
- /// 协程句柄
+ /// 拥有该协程生命周期的节点。
+ /// 要运行的协程枚举器。
+ /// 协程执行段。
+ /// 协程标签。
+ /// 可选取消令牌。
+ /// 新创建的协程句柄。
+ public static CoroutineHandle RunOwnedCoroutine(
+ Node owner,
+ IEnumerator coroutine,
+ Segment segment = Segment.Process,
+ string? tag = null,
+ CancellationToken cancellationToken = default)
+ {
+ return Instance.RunOwnedCoroutineOnInstance(owner, coroutine, segment, tag, cancellationToken);
+ }
+
+ ///
+ /// 在当前实例上运行协程。
+ ///
+ /// 要运行的协程枚举器。
+ /// 协程执行段。
+ /// 协程标签。
+ /// 可选取消令牌。
+ /// 新创建的协程句柄。
public CoroutineHandle RunCoroutineOnInstance(
IEnumerator? coroutine,
Segment segment = Segment.Process,
- string? tag = null)
+ string? tag = null,
+ CancellationToken cancellationToken = default)
{
if (coroutine == null)
- return default;
-
- return segment switch
{
- Segment.Process => ProcessScheduler.Run(coroutine, tag),
- Segment.ProcessIgnorePause => ProcessIgnorePauseScheduler.Run(coroutine, tag),
- Segment.PhysicsProcess => PhysicsScheduler.Run(coroutine, tag),
- Segment.DeferredProcess => DeferredScheduler.Run(coroutine, tag),
- _ => default
- };
+ return default;
+ }
+
+ return GetScheduler(segment).Run(coroutine, tag, group: null, cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// 在当前实例上运行归属于指定节点的协程。
+ ///
+ /// 拥有该协程的节点。
+ /// 要运行的协程枚举器。
+ /// 协程执行段。
+ /// 协程标签。
+ /// 可选取消令牌。
+ /// 新创建的协程句柄。
+ public CoroutineHandle RunOwnedCoroutineOnInstance(
+ Node? owner,
+ IEnumerator? coroutine,
+ Segment segment = Segment.Process,
+ string? tag = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (owner == null || coroutine == null || !IsNodeAlive(owner))
+ {
+ return default;
+ }
+
+ var handle = RunCoroutineOnInstance(
+ coroutine.CancelWith(owner),
+ segment,
+ tag,
+ cancellationToken);
+
+ if (!handle.IsValid)
+ {
+ return handle;
+ }
+
+ RegisterOwnedCoroutine(owner, handle);
+ return handle;
}
#endregion
@@ -355,60 +479,97 @@ public partial class Timing : Node
#region 协程控制 API
///
- /// 暂停指定的协程
+ /// 暂停指定的协程。
///
- /// 协程句柄
- /// 是否成功暂停
+ /// 协程句柄。
+ /// 如果成功暂停则返回 。
public static bool PauseCoroutine(CoroutineHandle handle)
{
return GetInstance(handle.Key)?.PauseOnInstance(handle) == true;
}
///
- /// 恢复指定的协程
+ /// 恢复指定的协程。
///
- /// 协程句柄
- /// 是否成功恢复
+ /// 协程句柄。
+ /// 如果成功恢复则返回 。
public static bool ResumeCoroutine(CoroutineHandle handle)
{
return GetInstance(handle.Key)?.ResumeOnInstance(handle) == true;
}
///
- /// 终止指定的协程
+ /// 终止指定的协程。
///
- /// 协程句柄
- /// 是否成功终止
+ /// 协程句柄。
+ /// 如果成功终止则返回 。
public static bool KillCoroutine(CoroutineHandle handle)
{
return GetInstance(handle.Key)?.KillOnInstance(handle) == true;
}
///
- /// 终止所有具有指定标签的协程
+ /// 终止某个节点归属的所有协程。
///
- /// 协程标签
- /// 被终止的协程数量
+ /// 协程归属节点。
+ /// 被终止的协程数量。
+ public static int KillCoroutines(Node owner)
+ {
+ return Instance.KillOwnedCoroutinesOnInstance(owner);
+ }
+
+ ///
+ /// 终止所有具有指定标签的协程。
+ ///
+ /// 协程标签。
+ /// 被终止的协程数量。
public static int KillCoroutines(string tag)
{
return Instance.KillByTagOnInstance(tag);
}
///
- /// 终止所有协程
+ /// 终止所有协程。
///
- /// 被终止的协程总数
+ /// 被终止的协程总数。
public static int KillAllCoroutines()
{
return Instance.ClearOnInstance();
}
///
- /// 在当前实例上暂停协程
- /// 尝试在所有调度器中查找并暂停指定协程
+ /// 根据协程句柄查询其当前快照。
///
- /// 协程句柄
- /// 是否成功暂停
+ /// 要查询的协程句柄。
+ /// 查询成功时返回快照。
+ /// 如果找到活跃协程则返回 。
+ public static bool TryGetCoroutineSnapshot(CoroutineHandle handle, out CoroutineSnapshot snapshot)
+ {
+ var instance = GetInstance(handle.Key);
+ if (instance == null)
+ {
+ snapshot = default;
+ return false;
+ }
+
+ return instance.TryGetSnapshotOnInstance(handle, out snapshot);
+ }
+
+ ///
+ /// 获取某个节点当前归属的活跃协程数量。
+ ///
+ /// 要查询的节点。
+ /// 该节点当前归属的活跃协程数量。
+ public static int GetOwnedCoroutineCount(Node owner)
+ {
+ return Instance.GetOwnedCoroutineCountOnInstance(owner);
+ }
+
+ ///
+ /// 在当前实例上暂停协程。
+ ///
+ /// 协程句柄。
+ /// 如果成功暂停则返回 。
private bool PauseOnInstance(CoroutineHandle handle)
{
return ProcessScheduler.Pause(handle)
@@ -418,11 +579,10 @@ public partial class Timing : Node
}
///
- /// 在当前实例上恢复协程
- /// 尝试在所有调度器中查找并恢复指定协程
+ /// 在当前实例上恢复协程。
///
- /// 协程句柄
- /// 是否成功恢复
+ /// 协程句柄。
+ /// 如果成功恢复则返回 。
private bool ResumeOnInstance(CoroutineHandle handle)
{
return ProcessScheduler.Resume(handle)
@@ -432,11 +592,10 @@ public partial class Timing : Node
}
///
- /// 在当前实例上终止协程
- /// 尝试在所有调度器中查找并终止指定协程
+ /// 在当前实例上终止协程。
///
- /// 协程句柄
- /// 是否成功终止
+ /// 协程句柄。
+ /// 如果成功终止则返回 。
private bool KillOnInstance(CoroutineHandle handle)
{
return ProcessScheduler.Kill(handle)
@@ -446,11 +605,10 @@ public partial class Timing : Node
}
///
- /// 在当前实例上根据标签终止协程
- /// 在所有调度器中查找并终止具有指定标签的协程
+ /// 在当前实例上根据标签终止协程。
///
- /// 协程标签
- /// 被终止的协程数量
+ /// 协程标签。
+ /// 被终止的协程数量。
private int KillByTagOnInstance(string tag)
{
var count = 0;
@@ -462,10 +620,45 @@ public partial class Timing : Node
}
///
- /// 清空当前实例上的所有协程
- /// 从所有调度器中清除协程
+ /// 在当前实例上终止某个节点归属的所有协程。
///
- /// 被清除的协程总数
+ /// 协程归属节点。
+ /// 被终止的协程数量。
+ private int KillOwnedCoroutinesOnInstance(Node owner)
+ {
+ var ownerId = owner.GetInstanceId();
+ if (!_ownedCoroutinesByNode.TryGetValue(ownerId, out var handles))
+ {
+ return 0;
+ }
+
+ var count = 0;
+ foreach (var handle in handles.ToArray())
+ {
+ if (KillOnInstance(handle))
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ ///
+ /// 获取某个节点当前归属的活跃协程数量。
+ ///
+ /// 要查询的节点。
+ /// 活跃归属协程数量。
+ private int GetOwnedCoroutineCountOnInstance(Node owner)
+ {
+ var ownerId = owner.GetInstanceId();
+ return _ownedCoroutinesByNode.TryGetValue(ownerId, out var handles) ? handles.Count : 0;
+ }
+
+ ///
+ /// 清空当前实例上的所有协程。
+ ///
+ /// 被清除的协程总数。
private int ClearOnInstance()
{
var count = 0;
@@ -481,60 +674,163 @@ public partial class Timing : Node
#region 工具方法
///
- /// 根据ID获取Timing实例
+ /// 根据 ID 获取 Timing 实例。
///
- /// 实例ID
- /// 对应的Timing实例或null
+ /// 实例 ID。
+ /// 对应的 Timing 实例;如果不存在则返回 。
public static Timing? GetInstance(byte id)
{
return id < ActiveInstances.Length ? ActiveInstances[id] : null;
}
-
///
- /// 检查节点是否处于有效状态
+ /// 检查节点是否处于有效状态。
///
- /// 要检查的节点
- /// 如果节点存在且有效则返回true,否则返回false
+ /// 要检查的节点。
+ /// 如果节点存在、实例有效、未进入删除队列且仍在场景树中,则返回 。
public static bool IsNodeAlive(Node? node)
{
- // 验证节点是否存在、实例是否有效、未被标记为删除且在场景树中
return node != null
&& IsInstanceValid(node)
&& !node.IsQueuedForDeletion()
&& node.IsInsideTree();
}
+ ///
+ /// 根据分段选择具体调度器。
+ ///
+ /// 目标执行段。
+ /// 与分段对应的协程调度器。
+ private CoroutineScheduler GetScheduler(Segment segment)
+ {
+ return segment switch
+ {
+ Segment.Process => ProcessScheduler,
+ Segment.ProcessIgnorePause => ProcessIgnorePauseScheduler,
+ Segment.PhysicsProcess => PhysicsScheduler,
+ Segment.DeferredProcess => DeferredScheduler,
+ _ => throw new ArgumentOutOfRangeException(nameof(segment), segment, "Unsupported coroutine segment.")
+ };
+ }
+
+ ///
+ /// 在当前实例上查询指定句柄的快照。
+ ///
+ /// 协程句柄。
+ /// 查询成功时返回快照。
+ /// 如果找到活跃协程则返回 。
+ private bool TryGetSnapshotOnInstance(CoroutineHandle handle, out CoroutineSnapshot snapshot)
+ {
+ return ProcessScheduler.TryGetSnapshot(handle, out snapshot)
+ || ProcessIgnorePauseScheduler.TryGetSnapshot(handle, out snapshot)
+ || PhysicsScheduler.TryGetSnapshot(handle, out snapshot)
+ || DeferredScheduler.TryGetSnapshot(handle, out snapshot);
+ }
+
+ ///
+ /// 注册节点归属协程,并在节点退树时强制终止该协程。
+ ///
+ /// 协程归属节点。
+ /// 要登记的协程句柄。
+ private void RegisterOwnedCoroutine(Node owner, CoroutineHandle handle)
+ {
+ var ownerId = owner.GetInstanceId();
+ var registration =
+ new OwnedCoroutineRegistration(owner, ownerId, handle, ownedHandle => _ = KillOnInstance(ownedHandle));
+
+ _ownedCoroutineRegistrations[handle] = registration;
+ if (!_ownedCoroutinesByNode.TryGetValue(ownerId, out var handles))
+ {
+ handles = new HashSet();
+ _ownedCoroutinesByNode[ownerId] = handles;
+ }
+
+ handles.Add(handle);
+ owner.TreeExiting += registration.OnOwnerTreeExiting;
+ }
+
+ ///
+ /// 在协程结束时解除节点归属回调并清理索引。
+ ///
+ /// 已结束的协程句柄。
+ /// 协程最终状态。
+ /// 若失败则为异常对象。
+ private void HandleCoroutineFinished(
+ CoroutineHandle handle,
+ CoroutineCompletionStatus status,
+ Exception? exception)
+ {
+ CleanupOwnedCoroutineRegistration(handle);
+ }
+
+ ///
+ /// 清理单个协程对应的节点归属注册。
+ ///
+ /// 要清理的协程句柄。
+ private void CleanupOwnedCoroutineRegistration(CoroutineHandle handle)
+ {
+ if (!_ownedCoroutineRegistrations.TryGetValue(handle, out var registration))
+ {
+ return;
+ }
+
+ if (registration.Owner.TryGetTarget(out var owner) && IsInstanceValid(owner))
+ {
+ owner.TreeExiting -= registration.OnOwnerTreeExiting;
+ }
+
+ if (_ownedCoroutinesByNode.TryGetValue(registration.OwnerId, out var handles))
+ {
+ handles.Remove(handle);
+ if (handles.Count == 0)
+ {
+ _ownedCoroutinesByNode.Remove(registration.OwnerId);
+ }
+ }
+
+ _ownedCoroutineRegistrations.Remove(handle);
+ }
+
+ ///
+ /// 清理所有已登记的节点归属回调。
+ ///
+ private void DetachAllOwnedRegistrations()
+ {
+ foreach (var handle in _ownedCoroutineRegistrations.Keys.ToArray())
+ {
+ CleanupOwnedCoroutineRegistration(handle);
+ }
+ }
+
#endregion
#region 延迟调用
///
- /// 延迟调用指定动作
+ /// 延迟执行指定动作。
///
- /// 延迟时间(秒)
- /// 要执行的动作
- /// 执行段
- /// 协程句柄
- public static CoroutineHandle CallDelayed(
- double delay,
- Action? action,
- Segment segment = Segment.Process)
+ /// 延迟时间,单位秒。
+ /// 到期时执行的动作。
+ /// 执行所在的协程段。
+ /// 新创建的协程句柄。
+ public static CoroutineHandle CallDelayed(double delay, Action? action, Segment segment = Segment.Process)
{
if (action == null)
+ {
return default;
+ }
return RunCoroutine(DelayedCallCoroutine(delay, action), segment);
}
///
- /// 延迟调用指定动作,支持取消条件
+ /// 延迟执行指定动作,并在节点失活时自动放弃执行。
///
- /// 延迟时间(秒)
- /// 要执行的动作
- /// 取消条件节点
- /// 执行段
- /// 协程句柄
+ /// 延迟时间,单位秒。
+ /// 到期时执行的动作。
+ /// 用于控制生命周期的节点。
+ /// 执行所在的协程段。
+ /// 新创建的协程句柄。
public static CoroutineHandle CallDelayed(
double delay,
Action? action,
@@ -542,44 +838,42 @@ public partial class Timing : Node
Segment segment = Segment.Process)
{
if (action == null)
+ {
return default;
+ }
- return RunCoroutine(
- DelayedCallWithCancelCoroutine(delay, action, cancelWith),
- segment);
+ return RunOwnedCoroutine(cancelWith, DelayedCallWithCancelCoroutine(delay, action, cancelWith), segment);
}
///
- /// 延迟调用协程实现
+ /// 延迟调用协程实现。
///
- /// 延迟时间
- /// 要执行的动作
- /// 协程枚举器
- private static IEnumerator DelayedCallCoroutine(
- double delay,
- Action action)
+ /// 延迟时间。
+ /// 要执行的动作。
+ /// 可直接交给调度器运行的协程枚举器。
+ private static IEnumerator DelayedCallCoroutine(double delay, Action action)
{
yield return new Delay(delay);
action();
}
///
- /// 带取消条件的延迟调用协程实现
+ /// 带节点生命周期判断的延迟调用协程实现。
///
- /// 延迟时间
- /// 要执行的动作
- /// 取消条件节点
- /// 协程枚举器
- private static IEnumerator DelayedCallWithCancelCoroutine(
- double delay,
- Action action,
+ /// 延迟时间。
+ /// 要执行的动作。
+ /// 生命周期检查节点。
+ /// 可直接交给调度器运行的协程枚举器。
+ private static IEnumerator DelayedCallWithCancelCoroutine(double delay, Action action,
Node cancelWith)
{
yield return new Delay(delay);
if (IsNodeAlive(cancelWith))
+ {
action();
+ }
}
#endregion
-}
+}
\ No newline at end of file
diff --git a/GFramework.csproj b/GFramework.csproj
index 6221359b..b34d43f5 100644
--- a/GFramework.csproj
+++ b/GFramework.csproj
@@ -62,6 +62,7 @@
+
@@ -102,6 +103,7 @@
+
@@ -128,6 +130,7 @@
+
diff --git a/GFramework.sln b/GFramework.sln
index 0cef5d4d..6d4eb45f 100644
--- a/GFramework.sln
+++ b/GFramework.sln
@@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GF
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Tests", "GFramework.Godot.SourceGenerators.Tests\GFramework.Godot.SourceGenerators.Tests.csproj", "{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -262,6 +264,18 @@ Global
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.Build.0 = Release|Any CPU
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = Release|Any CPU
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.Build.0 = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.Build.0 = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.Build.0 = Debug|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.ActiveCfg = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU
+ {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE