From 1c41c57d72cf530724529a56cdd43bd0208f4bab Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:06:35 +0800
Subject: [PATCH 1/6] =?UTF-8?q?feat(coroutine):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8D=8F=E7=A8=8B=E7=B3=BB=E7=BB=9F=E6=A0=B8=E5=BF=83=E7=BB=84?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8EGodot=E9=9B=86=E6=88=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现CoroutineMetadata类存储协程元数据信息
- 创建CoroutineScheduler协程调度器管理协程生命周期
- 添加CoroutineSlot类管理单个协程执行状态
- 实现GodotTimeSource时间源支持缩放和真实时间
- 添加Timing类提供Godot协程管理功能
- 实现CoroutineNodeExtensions扩展方法支持节点生命周期管理
- 支持协程分组、标签、优先级等功能
- 提供协程暂停、恢复、终止等控制接口
- 实现协程统计和快照功能
- 添加等待指令处理机制支持多种等待类型
---
.../Coroutine/CoroutineCompletionStatus.cs | 28 +
.../Coroutine/CoroutineExecutionStage.cs | 29 +
.../CoroutineSchedulerAdvancedTests.cs | 174 +++++
.../Coroutine/CoroutineMetadata.cs | 6 +
.../Coroutine/CoroutineScheduler.cs | 655 ++++++++++++++----
GFramework.Core/Coroutine/CoroutineSlot.cs | 6 +
.../Coroutine/CoroutineSnapshot.cs | 29 +
.../Coroutine/GodotTimeSourceTests.cs | 52 ++
.../GFramework.Godot.Tests.csproj | 23 +
.../Coroutine/CoroutineNodeExtensions.cs | 19 +
GFramework.Godot/Coroutine/GodotTimeSource.cs | 61 +-
GFramework.Godot/Coroutine/Timing.cs | 652 ++++++++++++-----
GFramework.csproj | 3 +
GFramework.sln | 14 +
14 files changed, 1409 insertions(+), 342 deletions(-)
create mode 100644 GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs
create mode 100644 GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs
create mode 100644 GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs
create mode 100644 GFramework.Core/Coroutine/CoroutineSnapshot.cs
create mode 100644 GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
create mode 100644 GFramework.Godot.Tests/GFramework.Godot.Tests.csproj
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
From 03346fbfe75ed10374987b4ec7f682cfc0436bcd Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:06:53 +0800
Subject: [PATCH 2/6] =?UTF-8?q?docs(coroutine):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E5=8D=8F=E7=A8=8B=E7=B3=BB=E7=BB=9F=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 重构 Core 协程系统文档,优化概述和核心概念说明
- 新增 Godot 协程系统集成文档
- 添加协程系统使用教程
- 更新等待指令说明,包括时间、条件、Task 和事件等待
- 补充协程控制、快照查询和生命周期管理相关内容
- 修正代码示例和 API 使用说明
---
docs/zh-CN/core/coroutine.md | 432 ++++----------
docs/zh-CN/godot/coroutine.md | 320 ++++------
docs/zh-CN/tutorials/coroutine-tutorial.md | 654 ++++-----------------
3 files changed, 332 insertions(+), 1074 deletions(-)
diff --git a/docs/zh-CN/core/coroutine.md b/docs/zh-CN/core/coroutine.md
index f10fa0fe..582111a5 100644
--- a/docs/zh-CN/core/coroutine.md
+++ b/docs/zh-CN/core/coroutine.md
@@ -1,61 +1,95 @@
---
title: 协程系统
-description: 协程系统提供基于 IEnumerator 的调度、等待和组合能力,可与事件、Task、命令与查询集成。
+description: 基于 IEnumerator 的协程调度系统,支持时间等待、阶段等待、Task 桥接、事件等待与运行时快照查询。
---
# 协程系统
## 概述
-GFramework 的 Core 协程系统基于 `IEnumerator` 构建,通过 `CoroutineScheduler`
-统一推进协程执行。它适合处理分帧逻辑、时间等待、条件等待、Task 桥接,以及事件驱动的异步流程。
+`GFramework.Core.Coroutine` 提供一个宿主无关的协程内核。它围绕 `CoroutineScheduler` 工作,统一处理:
-协程系统主要由以下部分组成:
+- `IEnumerator` 形式的协程推进
+- 时间等待、条件等待、Task 等待与事件等待
+- 标签、分组、暂停、恢复与终止
+- 取消令牌、完成状态查询与运行快照
+- 调度阶段语义,例如默认更新、固定更新和帧结束
-- `CoroutineScheduler`:负责运行、更新和控制协程
-- `CoroutineHandle`:用于标识协程实例并控制其状态
-- `IYieldInstruction`:定义等待行为的统一接口
-- `Instructions`:内置等待指令集合
-- `CoroutineHelper`:提供常用等待与生成器辅助方法
-- `Extensions`:提供 Task、组合、命令、查询和 Mediator 场景下的扩展方法
+Core 协程本身不依赖任何具体引擎;阶段语义是否真实成立,取决于宿主是否为调度器提供了匹配的执行阶段。
-## 核心概念
+## CoroutineScheduler
-### CoroutineScheduler
-
-`CoroutineScheduler` 是协程系统的核心调度器。构造时需要提供 `ITimeSource`,调度器会在每次 `Update()` 时读取时间增量并推进所有活跃协程。
+### 基础创建
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine;
-ITimeSource timeSource = /* 你的时间源实现 */;
-var scheduler = new CoroutineScheduler(timeSource);
+ITimeSource scaledTimeSource = /* 游戏时间 */;
+ITimeSource realtimeTimeSource = /* 真实时间,可选 */;
-var handle = scheduler.Run(MyCoroutine());
+var scheduler = new CoroutineScheduler(
+ scaledTimeSource,
+ realtimeTimeSource: realtimeTimeSource,
+ executionStage: CoroutineExecutionStage.Update);
-// 在你的主循环中推进协程
+var handle = scheduler.Run(MyCoroutine(), tag: "bootstrap", group: "loading");
+
+// 在宿主主循环中推进协程
scheduler.Update();
```
-如果需要统计信息,可以启用构造函数的 `enableStatistics` 参数。
+构造参数中最重要的两个语义是:
-### CoroutineHandle
+- `realtimeTimeSource`
+ - 如果提供,`WaitForSecondsRealtime` 会使用它的 `DeltaTime`
+ - 如果不提供,实时等待会退化为使用默认时间源
+- `executionStage`
+ - `Update`:默认阶段
+ - `FixedUpdate`:固定步阶段
+ - `EndOfFrame`:帧结束阶段
-`CoroutineHandle` 用于引用具体协程,并配合调度器进行控制:
+### 控制与完成状态
```csharp
-var handle = scheduler.Run(MyCoroutine(), tag: "gameplay", group: "battle");
+using var cts = new CancellationTokenSource();
-if (scheduler.IsCoroutineAlive(handle))
-{
- scheduler.Pause(handle);
- scheduler.Resume(handle);
- scheduler.Kill(handle);
-}
+var handle = scheduler.Run(
+ LoadResources(),
+ tag: "loading",
+ group: "bootstrap",
+ cancellationToken: cts.Token);
+
+scheduler.Pause(handle);
+scheduler.Resume(handle);
+scheduler.Kill(handle);
+
+var completionStatus = await scheduler.WaitForCompletionAsync(handle);
```
-### IYieldInstruction
+协程的最终结果由 `CoroutineCompletionStatus` 表示:
+
+- `Completed`
+- `Cancelled`
+- `Faulted`
+- `Unknown`
+
+### 快照与可观测性
+
+```csharp
+if (scheduler.TryGetSnapshot(handle, out var snapshot))
+{
+ Console.WriteLine(snapshot.State);
+ Console.WriteLine(snapshot.WaitingInstructionType);
+ Console.WriteLine(snapshot.ExecutionStage);
+}
+
+var allSnapshots = scheduler.GetActiveSnapshots();
+```
+
+快照适合做诊断、调试面板和运行中状态检查。
+
+## IYieldInstruction
协程通过 `yield return IYieldInstruction` 表达等待逻辑:
@@ -67,91 +101,40 @@ public interface IYieldInstruction
}
```
-## 基本用法
-
-### 创建简单协程
-
-```csharp
-using GFramework.Core.Abstractions.Coroutine;
-using GFramework.Core.Coroutine.Instructions;
-
-public IEnumerator SimpleCoroutine()
-{
- Console.WriteLine("开始");
-
- yield return new Delay(2.0);
- Console.WriteLine("2 秒后");
-
- yield return new WaitOneFrame();
- Console.WriteLine("下一帧");
-}
-```
-
-### 使用 CoroutineHelper
-
-`CoroutineHelper` 提供了一组常用等待和生成器辅助方法:
-
-```csharp
-using GFramework.Core.Coroutine;
-
-public IEnumerator HelperCoroutine()
-{
- yield return CoroutineHelper.WaitForSeconds(1.5);
- yield return CoroutineHelper.WaitForOneFrame();
- yield return CoroutineHelper.WaitForFrames(10);
- yield return CoroutineHelper.WaitUntil(() => isReady);
- yield return CoroutineHelper.WaitWhile(() => isLoading);
-}
-```
-
-除了直接返回等待指令,`CoroutineHelper` 也可以直接生成可运行的协程枚举器:
-
-```csharp
-scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
-scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
-
-using var cts = new CancellationTokenSource();
-scheduler.Run(CoroutineHelper.RepeatCallForever(1.0, () => Console.WriteLine("持续执行"), cts.Token));
-```
-
-### 控制协程状态
-
-```csharp
-var handle = scheduler.Run(LoadResources(), tag: "loading", group: "bootstrap");
-
-scheduler.Pause(handle);
-scheduler.Resume(handle);
-scheduler.Kill(handle);
-
-scheduler.KillByTag("loading");
-scheduler.PauseGroup("bootstrap");
-scheduler.ResumeGroup("bootstrap");
-scheduler.KillGroup("bootstrap");
-
-var cleared = scheduler.Clear();
-```
-
## 常用等待指令
### 时间与帧
```csharp
yield return new Delay(1.0);
+yield return new WaitForSecondsScaled(1.0);
yield return new WaitForSecondsRealtime(1.0);
yield return new WaitOneFrame();
yield return new WaitForNextFrame();
yield return new WaitForFrames(5);
-yield return new WaitForEndOfFrame();
yield return new WaitForFixedUpdate();
+yield return new WaitForEndOfFrame();
```
+语义说明:
+
+- `Delay` 与 `WaitForSecondsScaled`
+ - 使用调度器默认时间源推进
+- `WaitForSecondsRealtime`
+ - 优先使用调度器的 `realtimeTimeSource`
+- `WaitForFixedUpdate`
+ - 仅在 `CoroutineExecutionStage.FixedUpdate` 调度器中推进
+- `WaitForEndOfFrame`
+ - 仅在 `CoroutineExecutionStage.EndOfFrame` 调度器中推进
+
+如果宿主没有提供匹配阶段,这类阶段型等待不会自然完成。
+
### 条件等待
```csharp
yield return new WaitUntil(() => health > 0);
yield return new WaitWhile(() => isLoading);
yield return new WaitForPredicate(() => hp >= maxHp);
-yield return new WaitForPredicate(() => isBusy, waitForTrue: false);
yield return new WaitUntilOrTimeout(() => connected, timeoutSeconds: 5.0);
yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: true);
```
@@ -159,27 +142,14 @@ yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: tru
### Task 桥接
```csharp
-using System.Threading.Tasks;
using GFramework.Core.Coroutine.Extensions;
Task loadTask = LoadDataAsync();
yield return loadTask.AsCoroutineInstruction();
+
+var handle = scheduler.StartTaskAsCoroutine(LoadDataAsync());
```
-也可以将 `Task` 转成协程枚举器后直接交给调度器:
-
-```csharp
-var coroutine = LoadDataAsync().ToCoroutineEnumerator();
-var handle1 = scheduler.Run(coroutine);
-
-var handle2 = scheduler.StartTaskAsCoroutine(LoadDataAsync());
-```
-
-- `AsCoroutineInstruction()` 适合已经处在某个协程内部,只需要在当前位置等待 `Task` 完成的场景。
-- `ToCoroutineEnumerator()` 适合需要把 `Task` 先转换成 `IEnumerator`,再传给 `scheduler.Run(...)`、
- `Sequence(...)` 或其他只接受协程枚举器的 API。
-- `StartTaskAsCoroutine()` 适合已经持有 `CoroutineScheduler`,并希望把 `Task` 直接作为一个顶层协程启动的场景。
-
### 等待事件
```csharp
@@ -188,236 +158,52 @@ using GFramework.Core.Coroutine.Instructions;
public IEnumerator WaitForEventExample(IEventBus eventBus)
{
- using var waitEvent = new WaitForEvent(eventBus);
- yield return waitEvent;
-
- var eventData = waitEvent.EventData;
- Console.WriteLine($"玩家 {eventData!.PlayerId} 死亡");
-}
-```
-
-为事件等待附加超时:
-
-```csharp
-public IEnumerator WaitForEventWithTimeoutExample(IEventBus eventBus)
-{
- using var waitEvent = new WaitForEvent(eventBus);
- var timeoutWait = new WaitForEventWithTimeout(waitEvent, 5.0f);
-
- yield return timeoutWait;
-
- if (timeoutWait.IsTimeout)
- Console.WriteLine("等待超时");
- else
- Console.WriteLine($"玩家加入: {timeoutWait.EventData!.PlayerName}");
-}
-```
-
-等待两个事件中的任意一个:
-
-```csharp
-public IEnumerator WaitForEitherEvent(IEventBus eventBus)
-{
- using var wait = new WaitForMultipleEvents(eventBus);
+ using var wait = new WaitForEvent(eventBus);
yield return wait;
- if (wait.TriggeredBy == 1)
- Console.WriteLine($"Ready: {wait.FirstEventData}");
- else
- Console.WriteLine($"Quit: {wait.SecondEventData}");
+ Console.WriteLine(wait.EventData?.PlayerName);
}
```
-### 协程组合
+## CoroutineHelper
-等待子协程完成:
+`CoroutineHelper` 提供一组常用简写:
+
+```csharp
+yield return CoroutineHelper.WaitForSeconds(1.5);
+yield return CoroutineHelper.WaitForOneFrame();
+yield return CoroutineHelper.WaitForFrames(10);
+yield return CoroutineHelper.WaitUntil(() => isReady);
+yield return CoroutineHelper.WaitWhile(() => isLoading);
+```
+
+也可以直接生成可运行的协程枚举器:
+
+```csharp
+scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
+scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
+```
+
+## 协程组合
```csharp
public IEnumerator ParentCoroutine()
{
- Console.WriteLine("父协程开始");
-
yield return new WaitForCoroutine(ChildCoroutine());
-
- Console.WriteLine("子协程完成");
}
private IEnumerator ChildCoroutine()
{
- yield return CoroutineHelper.WaitForSeconds(1.0);
- Console.WriteLine("子协程执行");
+ yield return new Delay(1.0);
}
```
-等待多个句柄全部完成:
+如果需要等待多个顶层协程句柄,可以结合 `WaitForAllCoroutines` 或 `ParallelCoroutines(...)` 使用。
-```csharp
-public IEnumerator WaitForMultipleCoroutines(CoroutineScheduler scheduler)
-{
- var handles = new List
- {
- scheduler.Run(LoadTexture()),
- scheduler.Run(LoadAudio()),
- scheduler.Run(LoadModel())
- };
+## 建议
- yield return new WaitForAllCoroutines(scheduler, handles);
-
- Console.WriteLine("所有资源加载完成");
-}
-```
-
-### 进度等待
-
-```csharp
-public IEnumerator LoadingWithProgress()
-{
- yield return CoroutineHelper.WaitForProgress(
- duration: 3.0,
- onProgress: progress => Console.WriteLine($"加载进度: {progress * 100:F0}%"));
-}
-```
-
-## 扩展方法
-
-### 组合扩展
-
-`CoroutineComposeExtensions` 提供链式顺序组合能力:
-
-```csharp
-using GFramework.Core.Coroutine.Extensions;
-
-var chained =
- LoadConfig()
- .Then(() => Console.WriteLine("配置加载完成"))
- .Then(StartBattle());
-
-scheduler.Run(chained);
-```
-
-### 协程生成扩展
-
-`CoroutineExtensions` 提供了一些常用的协程生成器:
-
-```csharp
-using GFramework.Core.Coroutine.Extensions;
-
-var delayed = CoroutineExtensions.ExecuteAfter(2.0, () => Console.WriteLine("延迟执行"));
-var repeated = CoroutineExtensions.RepeatEvery(1.0, () => Console.WriteLine("tick"), count: 5);
-var progress = CoroutineExtensions.WaitForSecondsWithProgress(3.0, p => Console.WriteLine(p));
-
-scheduler.Run(delayed);
-scheduler.Run(repeated);
-scheduler.Run(progress);
-```
-
-顺序或并行组合多个协程:
-
-```csharp
-var sequence = CoroutineExtensions.Sequence(LoadConfig(), LoadScene(), StartBattle());
-scheduler.Run(sequence);
-
-var parallel = scheduler.ParallelCoroutines(LoadTexture(), LoadAudio(), LoadModel());
-scheduler.Run(parallel);
-```
-
-### Task 扩展
-
-`TaskCoroutineExtensions` 提供了三类扩展:
-
-- `AsCoroutineInstruction()`:把 `Task` / `Task` 包装成等待指令
-- `ToCoroutineEnumerator()`:把 `Task` / `Task` 转成协程枚举器
-- `StartTaskAsCoroutine()`:直接通过调度器启动 Task 协程
-
-### 命令、查询与 Mediator 扩展
-
-这些扩展都定义在 `GFramework.Core.Coroutine.Extensions` 命名空间中。
-
-### 命令协程
-
-```csharp
-using GFramework.Core.Coroutine.Extensions;
-
-public IEnumerator ExecuteCommand(IContextAware contextAware)
-{
- yield return contextAware.SendCommandCoroutineWithErrorHandler(
- new LoadSceneCommand(),
- ex => Console.WriteLine(ex.Message));
-}
-```
-
-如果命令执行后需要等待事件:
-
-```csharp
-public IEnumerator ExecuteCommandAndWaitEvent(IContextAware contextAware)
-{
- yield return contextAware.SendCommandAndWaitEventCoroutine(
- new LoadSceneCommand(),
- evt => Console.WriteLine($"场景加载完成: {evt.SceneName}"),
- timeout: 5.0f);
-}
-```
-
-### 查询协程
-
-`SendQueryCoroutine` 会同步执行查询,并通过回调返回结果:
-
-```csharp
-public IEnumerator QueryPlayer(IContextAware contextAware)
-{
- yield return contextAware.SendQueryCoroutine(
- new GetPlayerDataQuery { PlayerId = 1 },
- playerData => Console.WriteLine($"玩家名称: {playerData.Name}"));
-}
-```
-
-### Mediator 协程
-
-如果项目使用 `Mediator.IMediator`,还可以使用 `MediatorCoroutineExtensions`:
-
-```csharp
-public IEnumerator ExecuteMediatorCommand(IContextAware contextAware)
-{
- yield return contextAware.SendCommandCoroutine(
- new SaveArchiveCommand(),
- ex => Console.WriteLine(ex.Message));
-}
-```
-
-## 异常处理
-
-调度器会在协程抛出未捕获异常时触发 `OnCoroutineException`:
-
-```csharp
-scheduler.OnCoroutineException += (handle, exception) =>
-{
- Console.WriteLine($"协程 {handle} 异常: {exception.Message}");
-};
-```
-
-如果协程等待的是 `Task`,也可以通过 `WaitForTask` / `WaitForTask` 检查任务异常。
-
-## 常见问题
-
-### 协程什么时候执行?
-
-协程在调度器的 `Update()` 中推进。调度器每次更新都会先更新 `ITimeSource`,再推进所有活跃协程。
-
-### 协程是多线程的吗?
-
-不是。协程本身仍由调用 `Update()` 的线程推进,通常用于主线程上的分帧流程控制。
-
-### `Delay` 和 `CoroutineHelper.WaitForSeconds()` 有什么区别?
-
-两者表达的是同一类等待语义。`CoroutineHelper.WaitForSeconds()` 只是 `Delay` 的辅助构造方法。
-
-### 如何等待异步方法?
-
-在现有协程里等待 `Task` 时,优先使用 `yield return task.AsCoroutineInstruction()`;如果要把 `Task` 单独交给调度器启动,使用
-`scheduler.StartTaskAsCoroutine(task)`;如果中间还需要传给只接受协程枚举器的 API,则先调用 `task.ToCoroutineEnumerator()`。
-
-## 相关文档
-
-- [事件系统](/zh-CN/core/events)
-- [CQRS](/zh-CN/core/cqrs)
-- [协程系统教程](/zh-CN/tutorials/coroutine-tutorial)
+- 普通游戏时间等待优先使用 `Delay` 或 `WaitForSecondsScaled`
+- 只有宿主提供真实时间源时再使用 `WaitForSecondsRealtime`
+- 只有宿主显式区分阶段时才使用 `WaitForFixedUpdate` 与 `WaitForEndOfFrame`
+- 需要对接生命周期或外部取消时,优先传入 `CancellationToken`
+- 需要诊断线上状态时,优先使用 `TryGetSnapshot(...)` 和 `GetActiveSnapshots()`
diff --git a/docs/zh-CN/godot/coroutine.md b/docs/zh-CN/godot/coroutine.md
index 15171bf7..ce861f17 100644
--- a/docs/zh-CN/godot/coroutine.md
+++ b/docs/zh-CN/godot/coroutine.md
@@ -2,41 +2,30 @@
## 概述
-GFramework 的协程系统由两层组成:
+`GFramework.Godot.Coroutine` 在 Core 协程内核之上提供 Godot 宿主集成,负责把 Godot 的不同更新循环映射为真实的协程阶段语义:
-- `GFramework.Core.Coroutine` 提供通用调度器、`IYieldInstruction` 和一组等待指令。
-- `GFramework.Godot.Coroutine` 提供 Godot 环境下的运行入口、分段调度以及节点生命周期辅助方法。
+- `Segment.Process`
+- `Segment.ProcessIgnorePause`
+- `Segment.PhysicsProcess`
+- `Segment.DeferredProcess`
-Godot 集成层的核心入口包括:
+它同时补充了以下宿主能力:
-- `RunCoroutine(...)`
-- `Timing.RunGameCoroutine(...)`
-- `Timing.RunUiCoroutine(...)`
-- `Timing.CallDelayed(...)`
-- `CancelWith(...)`
+- 节点归属协程运行入口
+- 节点退树自动终止
+- Godot 真实时间源
+- 句柄控制与快照查询
-协程本身使用 `IEnumerator`。
+## 启动协程
-## 主要能力
-
-- 在 Godot 中按不同更新阶段运行协程
-- 等待时间、帧、条件、Task 和事件总线事件
-- 显式将协程与一个或多个 `Node` 的生命周期绑定
-- 通过 `CoroutineHandle` 暂停、恢复、终止协程
-- 将命令、查询、发布操作直接包装为协程运行
-
-## 基本用法
-
-### 启动协程
+### 直接运行枚举器
```csharp
-using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine;
-using Godot;
-public partial class MyNode : Node
+public partial class DemoNode : Node
{
public override void _Ready()
{
@@ -45,242 +34,131 @@ public partial class MyNode : Node
private IEnumerator Demo()
{
- GD.Print("开始执行");
-
- yield return new Delay(2.0);
- GD.Print("2 秒后继续执行");
-
+ GD.Print("start");
+ yield return new Delay(1.0);
yield return new WaitForEndOfFrame();
- GD.Print("当前帧结束后继续执行");
+ GD.Print("done");
}
}
```
-`RunCoroutine()` 默认在 `Segment.Process` 上运行,也就是普通帧更新阶段。
+默认情况下,`RunCoroutine()` 会在 `Segment.Process` 上运行。
-除了枚举器扩展方法,也可以直接使用 `Timing` 的静态入口:
+### 以 Node 作为生命周期所有者运行
+
+更推荐的方式是以节点为入口运行协程:
```csharp
-Timing.RunCoroutine(Demo());
-Timing.RunGameCoroutine(GameLoop());
-Timing.RunUiCoroutine(MenuAnimation());
-```
-
-### 显式绑定节点生命周期
-
-可以使用 `CancelWith(...)` 将协程与一个或多个节点的生命周期关联。
-
-```csharp
-using System.Collections.Generic;
-using GFramework.Core.Abstractions.Coroutine;
-using GFramework.Core.Coroutine.Instructions;
-using GFramework.Godot.Coroutine;
-using Godot;
-
-public partial class MyNode : Node
+public override void _Ready()
{
- public override void _Ready()
- {
- LongRunningTask()
- .CancelWith(this)
- .RunCoroutine();
- }
-
- private IEnumerator LongRunningTask()
- {
- while (true)
- {
- GD.Print("tick");
- yield return new Delay(1.0);
- }
- }
+ this.RunCoroutine(LongRunningTask(), Segment.Process, tag: "ui-blink");
}
```
-`CancelWith` 目前有三种重载:
+这会自动把协程登记为该节点归属协程,并在节点退出场景树时终止它。
-- `CancelWith(Node node)`
-- `CancelWith(Node node1, Node node2)`
-- `CancelWith(params Node[] nodes)`
+你仍然可以继续使用 `CancelWith(...)` 包装已有枚举器;它适合把一个协程显式绑定到多个节点生命周期。
-`CancelWith(...)` 内部通过 `Timing.IsNodeAlive(...)` 判断节点是否仍然有效。只要任一被监视的节点出现以下任一情况,包装后的协程就会停止继续枚举:
+## Segment 与阶段语义
-- 节点引用为 `null`
-- Godot 实例已经失效或已被释放
-- 节点已进入 `queue_free` / `IsQueuedForDeletion()`
-- 节点已退出场景树,`IsInsideTree()` 返回 `false`
+Godot 层会把不同 segment 映射为不同的 `CoroutineExecutionStage`:
-这意味着协程不只会在节点真正释放时停止;节点一旦退出场景树,下一次推进时也会停止。
-
-## Segment 分段
-
-Godot 层通过 `Segment` 决定协程挂在哪个调度器上:
-
-```csharp
-public enum Segment
-{
- Process,
- ProcessIgnorePause,
- PhysicsProcess,
- DeferredProcess
-}
-```
-
-- `Process`:普通 `_Process` 段,场景树暂停时不会推进。
-- `ProcessIgnorePause`:同样使用 process delta,但即使场景树暂停也会推进。
-- `PhysicsProcess`:在 `_PhysicsProcess` 段推进。
-- `DeferredProcess`:通过 `CallDeferred` 在当前帧之后推进,场景树暂停时不会推进。
+- `Segment.Process`
+ - 对应默认更新阶段
+ - 场景树暂停时不会推进
+- `Segment.ProcessIgnorePause`
+ - 同样对应默认更新阶段
+ - 场景树暂停时仍会推进
+- `Segment.PhysicsProcess`
+ - 对应固定更新阶段
+ - `WaitForFixedUpdate` 会在这里真实完成
+- `Segment.DeferredProcess`
+ - 对应帧结束阶段
+ - `WaitForEndOfFrame` 会在这里真实完成
示例:
```csharp
-UiAnimation().RunCoroutine(Segment.ProcessIgnorePause);
-PhysicsRoutine().RunCoroutine(Segment.PhysicsProcess);
+this.RunCoroutine(PhysicsRoutine(), Segment.PhysicsProcess);
+this.RunCoroutine(UiAnimation(), Segment.ProcessIgnorePause);
```
-如果你更偏向语义化入口,也可以直接使用:
+## 时间等待语义
+
+Godot 集成层为每个调度器同时提供了两套时间源:
+
+- 缩放时间
+ - 来自 `_Process` / `_PhysicsProcess` 的帧增量
+- 真实时间
+ - 来自 Godot 单调时钟,不受时间缩放和暂停影响
+
+因此:
+
+- `Delay` / `WaitForSecondsScaled` 使用宿主帧增量
+- `WaitForSecondsRealtime` 使用真实时间
+
+这意味着 UI 或暂停菜单中的协程可以安全使用 `WaitForSecondsRealtime` 保持真实计时。
+
+## 生命周期管理
+
+### 自动归属
```csharp
-Timing.RunGameCoroutine(GameLoop());
-Timing.RunUiCoroutine(MenuAnimation());
+var handle = this.RunCoroutine(LoadAvatar(), tag: "avatar");
```
-### 延迟调用
+### 手动绑定多个节点
-`Timing` 还提供了两个延迟调用快捷方法:
+```csharp
+LongRunningTask()
+ .CancelWith(this, panelNode)
+ .RunCoroutine();
+```
+
+### 主动清理
+
+```csharp
+Timing.KillCoroutine(handle);
+Timing.KillCoroutines(this);
+Timing.KillCoroutines("avatar");
+Timing.KillAllCoroutines();
+```
+
+## 调试与查询
+
+```csharp
+if (Timing.TryGetCoroutineSnapshot(handle, out var snapshot))
+{
+ GD.Print(snapshot.ExecutionStage);
+ GD.Print(snapshot.WaitingInstructionType);
+}
+
+var ownedCount = Timing.GetOwnedCoroutineCount(this);
+```
+
+实例级计数器:
+
+- `Timing.Instance.ProcessCoroutines`
+- `Timing.Instance.ProcessIgnorePauseCoroutines`
+- `Timing.Instance.PhysicsCoroutines`
+- `Timing.Instance.DeferredCoroutines`
+
+## 延迟调用
```csharp
Timing.CallDelayed(1.0, () => GD.Print("1 秒后执行"));
Timing.CallDelayed(1.0, () => GD.Print("节点仍然有效时执行"), this);
```
-第二个重载会在执行前检查传入节点是否仍然存活。
-
-## 常用等待指令
-
-以下类型可直接用于 `yield return`:
-
-### 时间与帧
-
-```csharp
-yield return new Delay(1.0);
-yield return new WaitForSecondsRealtime(1.0);
-yield return new WaitOneFrame();
-yield return new WaitForNextFrame();
-yield return new WaitForFrames(5);
-yield return new WaitForEndOfFrame();
-```
-
-说明:
-
-- `Delay` 是最直接的秒级等待。
-- `WaitForSecondsRealtime` 常用于需要独立计时语义的协程场景。
-- `WaitOneFrame`、`WaitForNextFrame`、`WaitForEndOfFrame` 用于帧级调度控制。
-
-### 条件等待
-
-```csharp
-yield return new WaitUntil(() => health > 0);
-yield return new WaitWhile(() => isLoading);
-```
-
-### Task 等待
-
-```csharp
-using System.Threading.Tasks;
-using GFramework.Core.Coroutine.Extensions;
-
-Task loadTask = LoadSomethingAsync();
-yield return loadTask.AsCoroutineInstruction();
-```
-
-也可以先把 `Task` 转成协程枚举器,再直接运行:
-
-```csharp
-LoadSomethingAsync()
- .ToCoroutineEnumerator()
- .RunCoroutine();
-```
-
-- 已经在一个协程内部时,优先使用 `yield return task.AsCoroutineInstruction()`,这样可以直接把 `Task` 嵌入当前协程流程。
-- 如果要把一个现成的 `Task` 当作独立协程入口交给 Godot 协程系统运行,再使用
- `task.ToCoroutineEnumerator().RunCoroutine()`。
-
-### 等待事件总线事件
-
-可以通过事件总线等待业务事件:
-
-```csharp
-using System.Collections.Generic;
-using GFramework.Core.Abstractions.Coroutine;
-using GFramework.Core.Abstractions.Events;
-using GFramework.Core.Coroutine.Instructions;
-
-private IEnumerator WaitForGameEvent(IEventBus eventBus)
-{
- using var wait = new WaitForEvent(eventBus);
- yield return wait;
-
- var evt = wait.EventData;
-}
-```
-
-如需为事件等待附加超时控制,可结合 `WaitForEventWithTimeout`。
-
-## 协程控制
-
-协程启动后会返回 `CoroutineHandle`,可用于控制运行状态:
-
-```csharp
-var handle = Demo().RunCoroutine(tag: "demo");
-
-Timing.PauseCoroutine(handle);
-Timing.ResumeCoroutine(handle);
-Timing.KillCoroutine(handle);
-
-Timing.KillCoroutines("demo");
-Timing.KillAllCoroutines();
-```
-
-如果希望在场景初始化阶段主动确保调度器存在,也可以调用:
-
-```csharp
-Timing.Prewarm();
-```
+第二个重载内部使用节点归属语义,因此节点退树后不会再触发动作。
## 与 IContextAware 集成
-`GFramework.Godot.Coroutine` 还提供了一组扩展方法,用于把命令、查询和通知直接包装成协程:
+Godot 层还提供以下扩展方法,用于把命令、查询和通知直接包装成协程并交给 Timing 调度:
- `RunCommandCoroutine(...)`
- `RunCommandCoroutine(...)`
- `RunQueryCoroutine(...)`
- `RunPublishCoroutine(...)`
-这些方法会把异步操作转换为协程,并交给 `RunCoroutine(...)` 调度执行。
-
-例如:
-
-```csharp
-public void StartCoroutines(IContextAware contextAware)
-{
- contextAware.RunCommandCoroutine(
- new EnterBattleCommand(),
- Segment.Process,
- tag: "battle");
-
- contextAware.RunQueryCoroutine(
- new LoadPlayerQuery(),
- Segment.ProcessIgnorePause,
- tag: "ui");
-}
-```
-
-这些扩展适合在 Godot 节点或控制器中直接启动和跟踪业务协程。
-
-## 相关文档
-
-- [Godot 概述](./index.md)
-- [Godot 扩展方法](./extensions.md)
-- [信号扩展](./signal.md)
-- [事件系统](../core/events.md)
+这些 API 仍然可以与 `Segment`、节点归属和标签控制一起使用。
diff --git a/docs/zh-CN/tutorials/coroutine-tutorial.md b/docs/zh-CN/tutorials/coroutine-tutorial.md
index 41e1024a..30000d15 100644
--- a/docs/zh-CN/tutorials/coroutine-tutorial.md
+++ b/docs/zh-CN/tutorials/coroutine-tutorial.md
@@ -1,6 +1,6 @@
---
title: 使用协程系统
-description: 学习如何使用协程系统实现异步操作和时间控制
+description: 学习如何在 GFramework 中创建调度器、运行协程,并结合时间、阶段、Task 与生命周期管理实现常见异步流程。
---
# 使用协程系统
@@ -9,590 +9,184 @@ description: 学习如何使用协程系统实现异步操作和时间控制
完成本教程后,你将能够:
-- 理解协程的基本概念和执行机制
-- 创建和启动协程
-- 使用各种等待指令控制协程执行
-- 在架构组件中使用协程
-- 实现常见的游戏逻辑(延迟执行、循环任务、事件等待)
-
-## 前置条件
-
-- 已安装 GFramework.Core NuGet 包
-- 了解 C# 基础语法和迭代器(IEnumerator)
-- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
-- 了解[生命周期管理](/zh-CN/core/lifecycle)
+- 创建并驱动 `CoroutineScheduler`
+- 编写 `IEnumerator` 协程
+- 区分缩放时间、真实时间与阶段等待
+- 使用句柄、取消令牌和快照查询控制协程
+- 在 Godot 中把协程绑定到节点生命周期
## 步骤 1:创建第一个协程
-首先,让我们创建一个简单的协程来理解基本概念。
-
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Instructions;
-namespace MyGame.Systems
+public sealed class TutorialLoop
{
- public class TutorialSystem : AbstractSystem
+ private readonly CoroutineScheduler _scheduler;
+
+ public TutorialLoop(ITimeSource timeSource)
{
- protected override void OnInit()
- {
- // 启动协程
- this.StartCoroutine(MyFirstCoroutine());
- }
+ _scheduler = new CoroutineScheduler(timeSource);
+ }
- ///
- /// 第一个协程示例
- ///
- private IEnumerator MyFirstCoroutine()
- {
- Console.WriteLine("协程开始执行");
+ public void Start()
+ {
+ _scheduler.Run(MyFirstCoroutine(), tag: "tutorial");
+ }
- // 等待 1 秒
- yield return CoroutineHelper.WaitForSeconds(1.0);
+ public void Tick()
+ {
+ _scheduler.Update();
+ }
- Console.WriteLine("1 秒后执行");
+ private IEnumerator MyFirstCoroutine()
+ {
+ Console.WriteLine("协程开始");
- // 等待 1 帧
- yield return CoroutineHelper.WaitForOneFrame();
+ yield return new Delay(1.0);
+ Console.WriteLine("1 秒后");
- Console.WriteLine("下一帧执行");
+ yield return new WaitOneFrame();
+ Console.WriteLine("下一帧");
- // 等待 5 帧
- yield return CoroutineHelper.WaitForFrames(5);
-
- Console.WriteLine("5 帧后执行");
- }
+ yield return new WaitForFrames(3);
+ Console.WriteLine("3 帧后");
}
}
```
-**代码说明**:
+关键点:
-- 协程方法返回 `IEnumerator`
-- 使用 `yield return` 返回等待指令
-- `this.StartCoroutine()` 扩展方法启动协程
-- `WaitForSeconds` 等待指定秒数
-- `WaitForOneFrame` 等待一帧
-- `WaitForFrames` 等待多帧
+- 协程返回类型必须是 `IEnumerator`
+- 调度器不会自动运行,你必须在宿主主循环中调用 `Update()`
+- `Run(...)` 返回 `CoroutineHandle`,后续控制都依赖这个句柄
-## 步骤 2:实现生命值自动恢复
-
-让我们实现一个实用的功能:玩家生命值自动恢复。
+## 步骤 2:控制协程生命周期
```csharp
-using GFramework.Core.Abstractions.Model;
-using GFramework.Core.Abstractions.Property;
-using GFramework.Core.Model;
+using var cts = new CancellationTokenSource();
-namespace MyGame.Models
+var handle = _scheduler.Run(
+ HealthRegenerationCoroutine(),
+ tag: "regen",
+ group: "player",
+ cancellationToken: cts.Token);
+
+_scheduler.Pause(handle);
+_scheduler.Resume(handle);
+
+// 外部取消会在下一次 Update 时生效
+cts.Cancel();
+
+var status = await _scheduler.WaitForCompletionAsync(handle);
+Console.WriteLine(status);
+```
+
+如果你需要观察运行中状态:
+
+```csharp
+if (_scheduler.TryGetSnapshot(handle, out var snapshot))
{
- public class PlayerModel : AbstractModel
- {
- // 当前生命值
- public BindableProperty Health { get; } = new(100);
-
- // 最大生命值
- public BindableProperty MaxHealth { get; } = new(100);
-
- // 是否启用自动恢复
- public BindableProperty AutoRegenEnabled { get; } = new(true);
-
- private CoroutineHandle? _regenHandle;
-
- protected override void OnInit()
- {
- // 启动生命值恢复协程
- StartHealthRegeneration();
- }
-
- ///
- /// 启动生命值恢复
- ///
- public void StartHealthRegeneration()
- {
- // 如果已经在运行,先停止
- if (_regenHandle.HasValue)
- {
- this.StopCoroutine(_regenHandle.Value);
- }
-
- // 启动新的恢复协程
- _regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
- }
-
- ///
- /// 停止生命值恢复
- ///
- public void StopHealthRegeneration()
- {
- if (_regenHandle.HasValue)
- {
- this.StopCoroutine(_regenHandle.Value);
- _regenHandle = null;
- }
- }
-
- ///
- /// 生命值恢复协程
- ///
- private IEnumerator HealthRegenerationCoroutine()
- {
- while (true)
- {
- // 等待 1 秒
- yield return CoroutineHelper.WaitForSeconds(1.0);
-
- // 检查是否启用自动恢复
- if (!AutoRegenEnabled.Value)
- continue;
-
- // 如果生命值未满,恢复 5 点
- if (Health.Value < MaxHealth.Value)
- {
- Health.Value = Math.Min(Health.Value + 5, MaxHealth.Value);
- Console.WriteLine($"生命值恢复: {Health.Value}/{MaxHealth.Value}");
- }
- }
- }
- }
+ Console.WriteLine(snapshot.State);
+ Console.WriteLine(snapshot.WaitingInstructionType);
}
```
-**代码说明**:
-
-- 使用 `while (true)` 创建无限循环协程
-- 保存协程句柄以便后续控制
-- 使用 `StopCoroutine` 停止协程
-- 协程中可以访问类成员变量
-
-## 步骤 3:实现技能冷却系统
-
-接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
+## 步骤 3:区分时间等待
```csharp
-using GFramework.Core.System;
-using System.Collections.Generic;
-
-namespace MyGame.Systems
+private IEnumerator CooldownCoroutine()
{
- public class SkillSystem : AbstractSystem
- {
- // 技能冷却状态
- private readonly Dictionary _skillCooldowns = new();
+ // 使用宿主默认时间
+ yield return new Delay(2.0);
- ///
- /// 使用技能
- ///
- public bool UseSkill(string skillName, double cooldownTime)
- {
- // 检查是否在冷却中
- if (_skillCooldowns.TryGetValue(skillName, out var isOnCooldown) && isOnCooldown)
- {
- Console.WriteLine($"技能 {skillName} 冷却中...");
- return false;
- }
-
- // 执行技能
- Console.WriteLine($"使用技能: {skillName}");
-
- // 启动冷却协程
- this.StartCoroutine(SkillCooldownCoroutine(skillName, cooldownTime));
-
- return true;
- }
-
- ///
- /// 技能冷却协程
- ///
- private IEnumerator SkillCooldownCoroutine(string skillName, double cooldownTime)
- {
- // 标记为冷却中
- _skillCooldowns[skillName] = true;
-
- Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
-
- // 等待冷却时间
- yield return CoroutineHelper.WaitForSeconds(cooldownTime);
-
- // 冷却结束
- _skillCooldowns[skillName] = false;
- Console.WriteLine($"技能 {skillName} 冷却完成");
- }
-
- ///
- /// 带进度显示的技能冷却
- ///
- private IEnumerator SkillCooldownWithProgressCoroutine(
- string skillName,
- double cooldownTime)
- {
- _skillCooldowns[skillName] = true;
-
- // 使用 WaitForProgress 显示冷却进度
- yield return CoroutineHelper.WaitForProgress(
- duration: cooldownTime,
- onProgress: progress =>
- {
- Console.WriteLine($"技能 {skillName} 冷却进度: {progress * 100:F0}%");
- }
- );
-
- _skillCooldowns[skillName] = false;
- Console.WriteLine($"技能 {skillName} 冷却完成");
- }
- }
+ // 使用真实时间,需要调度器提供 realtimeTimeSource
+ yield return new WaitForSecondsRealtime(2.0);
}
```
-**代码说明**:
+建议:
-- 使用字典管理多个技能的冷却状态
-- 每个技能使用独立的协程管理冷却
-- `WaitForProgress` 可以在等待期间执行回调
-- 协程结束后自动清理冷却状态
+- 普通游戏逻辑优先使用 `Delay`
+- 暂停菜单、真实倒计时、网络超时等场景使用 `WaitForSecondsRealtime`
-## 步骤 4:等待事件触发
+## 步骤 4:使用阶段等待
-实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。
+只有宿主为调度器提供了匹配阶段时,阶段等待才会真实生效:
```csharp
-using GFramework.Core.Abstractions.Events;
+var fixedScheduler = new CoroutineScheduler(
+ fixedTimeSource,
+ executionStage: CoroutineExecutionStage.FixedUpdate);
+
+private IEnumerator PhysicsCoroutine()
+{
+ yield return new WaitForFixedUpdate();
+ Console.WriteLine("下一次固定步到达");
+}
+```
+
+同理,`WaitForEndOfFrame` 需要运行在 `CoroutineExecutionStage.EndOfFrame` 的调度器上。
+
+## 步骤 5:等待 Task
+
+```csharp
+using GFramework.Core.Coroutine.Extensions;
+
+private IEnumerator LoadCoroutine()
+{
+ var task = LoadDataAsync();
+ yield return task.AsCoroutineInstruction();
+ Console.WriteLine("Task 已完成");
+}
+```
+
+如果你已经持有调度器,也可以直接把 `Task` 作为顶层协程启动:
+
+```csharp
+var handle = _scheduler.StartTaskAsCoroutine(LoadDataAsync());
+```
+
+## 步骤 6:在 Godot 中绑定 Node 生命周期
+
+```csharp
+using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
+using GFramework.Godot.Coroutine;
+using Godot;
-namespace MyGame.Systems
+public partial class DemoNode : Node
{
- // 任务完成事件
- public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
-
- public class QuestSystem : AbstractSystem
+ public override void _Ready()
{
- ///
- /// 开始任务并等待完成
- ///
- public void StartQuest(int questId, string questName)
+ // 推荐:节点作为所有者运行协程
+ this.RunCoroutine(BlinkCoroutine(), Segment.ProcessIgnorePause, tag: "blink");
+ }
+
+ private IEnumerator BlinkCoroutine()
+ {
+ while (true)
{
- this.StartCoroutine(QuestCoroutine(questId, questName));
- }
-
- ///
- /// 任务协程
- ///
- private IEnumerator QuestCoroutine(int questId, string questName)
- {
- Console.WriteLine($"任务开始: {questName}");
-
- // 获取事件总线
- var eventBus = this.GetService();
-
- // 等待任务完成事件
- var waitEvent = new WaitForEvent(
- eventBus,
- evt => evt.QuestId == questId // 过滤条件
- );
-
- yield return waitEvent;
-
- // 获取事件数据
- var completedEvent = waitEvent.EventData;
- Console.WriteLine($"任务完成: {completedEvent.QuestName}");
-
- // 发放奖励
- GiveReward(questId);
- }
-
- ///
- /// 带超时的任务
- ///
- private IEnumerator TimedQuestCoroutine(
- int questId,
- string questName,
- double timeLimit)
- {
- Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
-
- var eventBus = this.GetService();
-
- // 等待事件,带超时
- var waitEvent = new WaitForEventWithTimeout(
- eventBus,
- timeout: timeLimit,
- predicate: evt => evt.QuestId == questId
- );
-
- yield return waitEvent;
-
- if (waitEvent.IsTimeout)
- {
- Console.WriteLine($"任务超时失败: {questName}");
- }
- else
- {
- Console.WriteLine($"任务完成: {questName}");
- GiveReward(questId);
- }
- }
-
- private void GiveReward(int questId)
- {
- Console.WriteLine($"发放任务 {questId} 的奖励");
+ Visible = !Visible;
+ yield return new WaitForSecondsRealtime(0.5);
}
}
}
```
-**代码说明**:
+当 `DemoNode` 退出场景树时,上面的协程会被自动终止。
-- `WaitForEvent` 等待特定事件触发
-- 可以使用 `predicate` 参数过滤事件
-- `WaitForEventWithTimeout` 支持超时机制
-- 通过 `EventData` 属性获取事件数据
-
-## 步骤 5:协程组合与嵌套
-
-实现一个复杂的游戏流程,展示如何组合多个协程。
+如果你需要绑定多个节点,可以继续使用:
```csharp
-namespace MyGame.Systems
-{
- public class GameFlowSystem : AbstractSystem
- {
- ///
- /// 游戏开始流程
- ///
- public void StartGame()
- {
- this.StartCoroutine(GameStartSequence());
- }
-
- ///
- /// 游戏开始序列
- ///
- private IEnumerator GameStartSequence()
- {
- Console.WriteLine("=== 游戏开始 ===");
-
- // 1. 显示标题
- yield return ShowTitle();
-
- // 2. 加载资源
- yield return LoadResources();
-
- // 3. 初始化玩家
- yield return InitializePlayer();
-
- // 4. 播放开场动画
- yield return PlayOpeningAnimation();
-
- Console.WriteLine("=== 游戏准备完成 ===");
- }
-
- ///
- /// 显示标题
- ///
- private IEnumerator ShowTitle()
- {
- Console.WriteLine("显示游戏标题...");
- yield return CoroutineHelper.WaitForSeconds(2.0);
- Console.WriteLine("标题显示完成");
- }
-
- ///
- /// 加载资源
- ///
- private IEnumerator LoadResources()
- {
- Console.WriteLine("开始加载资源...");
-
- // 并行加载多个资源
- var loadTextures = LoadTexturesCoroutine();
- var loadAudio = LoadAudioCoroutine();
- var loadModels = LoadModelsCoroutine();
-
- // 等待所有资源加载完成
- yield return new WaitForAllCoroutines(
- this.GetCoroutineScheduler(),
- loadTextures,
- loadAudio,
- loadModels
- );
-
- Console.WriteLine("所有资源加载完成");
- }
-
- private IEnumerator LoadTexturesCoroutine()
- {
- Console.WriteLine(" 加载纹理...");
- yield return CoroutineHelper.WaitForSeconds(1.0);
- Console.WriteLine(" 纹理加载完成");
- }
-
- private IEnumerator LoadAudioCoroutine()
- {
- Console.WriteLine(" 加载音频...");
- yield return CoroutineHelper.WaitForSeconds(1.5);
- Console.WriteLine(" 音频加载完成");
- }
-
- private IEnumerator LoadModelsCoroutine()
- {
- Console.WriteLine(" 加载模型...");
- yield return CoroutineHelper.WaitForSeconds(0.8);
- Console.WriteLine(" 模型加载完成");
- }
-
- private IEnumerator InitializePlayer()
- {
- Console.WriteLine("初始化玩家...");
- yield return CoroutineHelper.WaitForSeconds(0.5);
- Console.WriteLine("玩家初始化完成");
- }
-
- private IEnumerator PlayOpeningAnimation()
- {
- Console.WriteLine("播放开场动画...");
- yield return CoroutineHelper.WaitForSeconds(3.0);
- Console.WriteLine("开场动画播放完成");
- }
-
- ///
- /// 获取协程调度器
- ///
- private CoroutineScheduler GetCoroutineScheduler()
- {
- // 从架构服务中获取
- return this.GetService();
- }
- }
-}
+BlinkCoroutine()
+ .CancelWith(this, anotherNode)
+ .RunCoroutine();
```
-**代码说明**:
-
-- 使用 `yield return` 调用其他协程实现嵌套
-- `WaitForAllCoroutines` 并行执行多个协程
-- 协程可以像函数一样组合和复用
-- 清晰的流程控制,避免回调嵌套
-
-## 完整代码
-
-### GameArchitecture.cs
-
-```csharp
-using GFramework.Core.Architecture;
-
-namespace MyGame
-{
- public class GameArchitecture : Architecture
- {
- public static IArchitecture Interface { get; private set; }
-
- protected override void Init()
- {
- Interface = this;
-
- // 注册 Model
- RegisterModel(new PlayerModel());
-
- // 注册 System
- RegisterSystem(new TutorialSystem());
- RegisterSystem(new SkillSystem());
- RegisterSystem(new QuestSystem());
- RegisterSystem(new GameFlowSystem());
- }
- }
-}
-```
-
-### 测试代码
-
-```csharp
-using MyGame;
-using MyGame.Systems;
-
-// 初始化架构
-var architecture = new GameArchitecture();
-architecture.Initialize();
-await architecture.WaitUntilReadyAsync();
-
-// 测试技能系统
-var skillSystem = architecture.GetSystem();
-skillSystem.UseSkill("火球术", 3.0);
-await Task.Delay(1000);
-skillSystem.UseSkill("火球术", 3.0); // 冷却中
-await Task.Delay(3000);
-skillSystem.UseSkill("火球术", 3.0); // 冷却完成
-
-// 测试任务系统
-var questSystem = architecture.GetSystem();
-questSystem.StartQuest(1, "击败史莱姆");
-
-// 模拟任务完成
-await Task.Delay(2000);
-var eventBus = architecture.GetService();
-eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
-
-// 测试游戏流程
-var gameFlowSystem = architecture.GetSystem();
-gameFlowSystem.StartGame();
-```
-
-## 运行结果
-
-运行程序后,你将看到类似以下的输出:
-
-```
-协程开始执行
-1 秒后执行
-下一帧执行
-5 帧后执行
-
-使用技能: 火球术
-技能 火球术 开始冷却 3.0 秒
-技能 火球术 冷却中...
-技能 火球术 冷却完成
-使用技能: 火球术
-
-任务开始: 击败史莱姆
-任务完成: 击败史莱姆
-发放任务 1 的奖励
-
-=== 游戏开始 ===
-显示游戏标题...
-标题显示完成
-开始加载资源...
- 加载纹理...
- 加载音频...
- 加载模型...
- 模型加载完成
- 纹理加载完成
- 音频加载完成
-所有资源加载完成
-初始化玩家...
-玩家初始化完成
-播放开场动画...
-开场动画播放完成
-=== 游戏准备完成 ===
-```
-
-**验证步骤**:
-
-1. 协程按预期顺序执行
-2. 技能冷却系统正常工作
-3. 事件等待功能正确
-4. 并行加载资源成功
-
## 下一步
-恭喜!你已经掌握了协程系统的基本用法。接下来可以学习:
-
-- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 使用协程实现状态转换
-- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在协程中加载资源
-- [使用事件系统](/zh-CN/core/events) - 协程与事件系统集成
-
-## 相关文档
-
-- [协程系统](/zh-CN/core/coroutine) - 协程系统详细说明
-- [事件系统](/zh-CN/core/events) - 事件系统详解
-- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
-- [System 层](/zh-CN/core/system) - System 详细说明
+- Core 侧更完整的 API 说明见 [Core 协程系统](/zh-CN/core/coroutine)
+- Godot 集成细节见 [Godot 协程系统](/zh-CN/godot/coroutine)
From ccffb121b37ea225434175b857db60e673664ba2 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:18:29 +0800
Subject: [PATCH 3/6] =?UTF-8?q?fix(coroutine):=20=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E5=8D=8F=E7=A8=8B=E5=AE=8C=E6=88=90=E7=8A=B6=E6=80=81=E5=A4=84?=
=?UTF-8?q?=E7=90=86=E4=B8=AD=E7=9A=84=E5=BC=82=E5=B8=B8=E6=83=85=E5=86=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为 CoroutineCompletionStatus 枚举添加默认分支处理
- 抛出 ArgumentOutOfRangeException 以处理不支持的协程完成状态
- 防止因未知状态值导致的运行时错误
- 提高协程调度器的健壮性和错误处理能力
---
.../Coroutine/CoroutineScheduler.cs | 30 ++++++++++---------
1 file changed, 16 insertions(+), 14 deletions(-)
diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs
index 0aeb31f2..05fa1f06 100644
--- a/GFramework.Core/Coroutine/CoroutineScheduler.cs
+++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs
@@ -140,20 +140,16 @@ public sealed class CoroutineScheduler(
/// 包含所有活跃协程的快照列表。
public IReadOnlyList GetActiveSnapshots()
{
- var snapshots = new List(_metadata.Count);
-
- foreach (var pair in _metadata)
- {
- var slot = _slots[pair.Value.SlotIndex];
- if (slot == null)
+ return _metadata
+ .Select(pair => pair.Value)
+ .Select(meta => new
{
- continue;
- }
-
- snapshots.Add(CreateSnapshot(pair.Value, slot));
- }
-
- return snapshots;
+ Metadata = meta,
+ Slot = _slots[meta.SlotIndex]
+ })
+ .Where(item => item.Slot is not null)
+ .Select(item => CreateSnapshot(item.Metadata, item.Slot!))
+ .ToArray();
}
///
@@ -689,6 +685,12 @@ public sealed class CoroutineScheduler(
case CoroutineCompletionStatus.Cancelled:
meta.State = CoroutineState.Cancelled;
break;
+
+ default:
+ throw new ArgumentOutOfRangeException(
+ nameof(completionStatus),
+ completionStatus,
+ "Unsupported coroutine completion status.");
}
}
@@ -855,7 +857,7 @@ public sealed class CoroutineScheduler(
/// 协程元数据。
/// 协程槽位。
/// 与当前槽位一致的只读快照。
- private CoroutineSnapshot CreateSnapshot(CoroutineMetadata metadata, CoroutineSlot slot)
+ private static CoroutineSnapshot CreateSnapshot(CoroutineMetadata metadata, CoroutineSlot slot)
{
return new CoroutineSnapshot(
slot.Handle,
From 6cac882fb43a3fd2f897fcd22ac14c60906729d7 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sun, 5 Apr 2026 15:21:51 +0800
Subject: [PATCH 4/6] =?UTF-8?q?refactor(coroutine):=20=E7=AE=80=E5=8C=96?=
=?UTF-8?q?=E5=8D=8F=E7=A8=8B=E8=B0=83=E5=BA=A6=E5=99=A8=E4=B8=AD=E7=9A=84?=
=?UTF-8?q?=E6=89=A7=E8=A1=8C=E9=98=B6=E6=AE=B5=E8=AE=BF=E9=97=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 移除私有字段 _executionStage,直接使用构造函数参数 executionStage
- 更新 ExecutionStage 属性实现,直接返回构造函数参数
- 修改协程元数据设置时使用参数而非私有字段
- 调整等待指令判断逻辑,直接比较参数值
---
GFramework.Core/Coroutine/CoroutineScheduler.cs | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs
index 05fa1f06..b11a5e57 100644
--- a/GFramework.Core/Coroutine/CoroutineScheduler.cs
+++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs
@@ -38,7 +38,6 @@ public sealed class CoroutineScheduler(
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();
@@ -69,7 +68,7 @@ public sealed class CoroutineScheduler(
///
/// 获取当前调度器代表的执行阶段。
///
- public CoroutineExecutionStage ExecutionStage => _executionStage;
+ public CoroutineExecutionStage ExecutionStage => executionStage;
///
/// 获取活跃协程数量。
@@ -234,7 +233,7 @@ public sealed class CoroutineScheduler(
_slots[slotIndex] = slot;
_metadata[handle] = new CoroutineMetadata
{
- ExecutionStage = _executionStage,
+ ExecutionStage = executionStage,
Group = group,
Priority = priority,
SlotIndex = slotIndex,
@@ -756,8 +755,8 @@ public sealed class CoroutineScheduler(
{
return instruction switch
{
- WaitForFixedUpdate => _executionStage == CoroutineExecutionStage.FixedUpdate,
- WaitForEndOfFrame => _executionStage == CoroutineExecutionStage.EndOfFrame,
+ WaitForFixedUpdate => executionStage == CoroutineExecutionStage.FixedUpdate,
+ WaitForEndOfFrame => executionStage == CoroutineExecutionStage.EndOfFrame,
_ => true
};
}
From 20534511855b2d1480afac14fb453ae190de4f80 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 6 Apr 2026 00:33:37 +0800
Subject: [PATCH 5/6] =?UTF-8?q?feat(coroutine):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=8D=8F=E7=A8=8B=E8=B0=83=E5=BA=A6=E5=99=A8=E5=92=8C=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了 CoroutineScheduler 类,支持协程的运行、暂停、恢复和终止管理
- 添加了协程槽位管理机制 CoroutineSlot,用于跟踪单个协程状态
- 实现了协程的优先级、标签和分组功能,支持批量操作
- 集成了等待指令系统,包括 WaitForSecondsRealtime、WaitForFixedUpdate 等
- 添加了协程统计功能和异常处理机制
- 实现了 Godot 平台的时间源适配器 GodotTimeSource
- 创建了协程调度器的高级功能测试用例
- 添加了 Timing 节点用于在 Godot 引擎中管理协程生命周期
---
.../CoroutineSchedulerAdvancedTests.cs | 59 +++++++++++++++++++
.../Coroutine/CoroutineScheduler.cs | 36 ++++++++++-
GFramework.Core/Coroutine/CoroutineSlot.cs | 6 ++
.../Coroutine/GodotTimeSourceTests.cs | 22 ++++++-
GFramework.Godot/Coroutine/GodotTimeSource.cs | 9 +--
GFramework.Godot/Coroutine/Timing.cs | 34 +++++++++--
6 files changed, 152 insertions(+), 14 deletions(-)
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!);
+ }
+
///
/// 检查节点是否处于有效状态。
///
From d21370787bee380f7874fdd1eb0c8758d9b72d2e Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 6 Apr 2026 00:37:32 +0800
Subject: [PATCH 6/6] =?UTF-8?q?test(godot):=20=E6=B7=BB=E5=8A=A0Godot?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95=E9=A1=B9=E7=9B=AE=E5=B9=B6=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?CI=E6=B5=81=E6=B0=B4=E7=BA=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在CI工作流中添加GFramework.Godot.Tests项目的测试执行
- 配置dotnet test命令运行Godot测试套件
- 添加TRX格式的日志输出和测试结果目录设置
- 更新Godot模块的全局引用配置
- 在Timing.cs中添加必要的命名空间引用
- 在GodotTimeSourceTests.cs中添加NUnit框架引用
---
.github/workflows/ci.yml | 5 +++++
GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs | 1 +
GFramework.Godot/Coroutine/Timing.cs | 3 +++
GFramework.Godot/GlobalUsings.cs | 3 ++-
4 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac273db5..bb3fb6b9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -156,6 +156,11 @@ jobs:
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
--results-directory TestResults &
+ dotnet test GFramework.Godot.Tests \
+ -c Release \
+ --no-build \
+ --logger "trx;LogFileName=godot-$RANDOM.trx" \
+ --results-directory TestResults &
# 等待所有后台测试完成
wait
diff --git a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
index 3f46fb1b..752310f8 100644
--- a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
+++ b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using GFramework.Godot.Coroutine;
+using NUnit.Framework;
namespace GFramework.Godot.Tests.Coroutine;
diff --git a/GFramework.Godot/Coroutine/Timing.cs b/GFramework.Godot/Coroutine/Timing.cs
index 774bfcc1..d8913a7e 100644
--- a/GFramework.Godot/Coroutine/Timing.cs
+++ b/GFramework.Godot/Coroutine/Timing.cs
@@ -1,5 +1,8 @@
using System.Reflection;
using GFramework.Core.Abstractions.Coroutine;
+using GFramework.Core.Coroutine;
+using GFramework.Core.Coroutine.Instructions;
+using GFramework.Godot.Extensions;
namespace GFramework.Godot.Coroutine;
diff --git a/GFramework.Godot/GlobalUsings.cs b/GFramework.Godot/GlobalUsings.cs
index 4d271811..41d45db7 100644
--- a/GFramework.Godot/GlobalUsings.cs
+++ b/GFramework.Godot/GlobalUsings.cs
@@ -15,4 +15,5 @@ global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
-global using System.Threading.Tasks;
\ No newline at end of file
+global using System.Threading.Tasks;
+global using Godot;
\ No newline at end of file