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.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; + +/// +/// 表示协程调度器当前所处的执行阶段。 +/// +/// +/// 某些等待指令具有阶段语义,例如 WaitForFixedUpdateWaitForEndOfFrame。 +/// 宿主应为这些语义提供匹配的调度器阶段,否则这类等待不会自然完成。 +/// +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..f262cd55 --- /dev/null +++ b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs @@ -0,0 +1,233 @@ +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)); + } + + /// + /// 验证完成状态缓存有固定上限,避免无限增长。 + /// + [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/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..0a24ce6d 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,48 @@ 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 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(); + 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,140 +59,263 @@ 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() + { + return _metadata + .Select(pair => pair.Value) + .Select(meta => new + { + Metadata = meta, + Slot = _slots[meta.SlotIndex] + }) + .Where(item => item.Slot is not null) + .Select(item => CreateSnapshot(item.Metadata, item.Slot!)) + .ToArray(); + } + + /// + /// 获取指定协程的完成任务。 + /// + /// 要等待完成的协程句柄。 + /// 返回表示协程最终结果的任务。 + /// + /// 如果句柄已经结束,则返回一个已完成任务。 + /// 如果句柄无效或结果未知,则任务结果为 。 + /// + 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++; var slot = new CoroutineSlot { + CancellationToken = cancellationToken, Enumerator = coroutine, State = CoroutineState.Running, Handle = handle, 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 +327,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 +380,29 @@ public sealed class CoroutineScheduler( } /// - /// 处理协程的yield指令 + /// 处理协程返回的等待指令。 /// - /// 协程槽位 - /// yield指令 + /// 当前协程槽位。 + /// 当前等待指令。 private void HandleYieldInstruction(CoroutineSlot slot, IYieldInstruction instruction) { switch (instruction) { - // 处理 WaitForCoroutine 指令 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; } + default: slot.Waiting = instruction; break; @@ -250,56 +414,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 +485,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 +545,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,35 +571,49 @@ 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(); _grouped.Clear(); _waiting.Clear(); + _completionStatuses.Clear(); + _completionStatusOrder.Clear(); _nextSlot = 0; ActiveCoroutineCount = 0; _pausedCount = 0; + UpdateStatisticsSnapshot(); return count; } @@ -424,21 +623,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; + { + HandleYieldInstruction(slot, slot.Enumerator.Current); + } } catch (Exception ex) { @@ -447,75 +652,93 @@ 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; + + default: + throw new ArgumentOutOfRangeException( + nameof(completionStatus), + completionStatus, + "Unsupported coroutine completion status."); + } + } + + 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); + RecordCompletionStatus(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 +750,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 static 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 +892,41 @@ public sealed class CoroutineScheduler( } /// - /// 为协程添加标签 + /// 更新统计对象中的活动快照数据。 /// - /// 标签名称 - /// 协程句柄 + private void UpdateStatisticsSnapshot() + { + if (_statistics == null) + { + return; + } + + _statistics.ActiveCount = ActiveCoroutineCount; + _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); + } + } + + /// + /// 为协程添加标签。 + /// + /// 标签名称。 + /// 协程句柄。 private void AddTag(string tag, CoroutineHandle handle) { if (!_tagged.TryGetValue(tag, out var set)) @@ -566,29 +940,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 +980,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..1fb11994 100644 --- a/GFramework.Core/Coroutine/CoroutineSlot.cs +++ b/GFramework.Core/Coroutine/CoroutineSlot.cs @@ -7,6 +7,18 @@ namespace GFramework.Core.Coroutine; /// internal sealed class CoroutineSlot { + /// + /// 由外部取消令牌创建的注册。 + /// 调度器在协程结束时必须释放该注册,避免泄漏取消回调。 + /// + public CancellationTokenRegistration CancellationRegistration; + + /// + /// 创建该协程时传入的取消令牌。 + /// 当协程启动子协程时,会把同一个取消令牌继续传递下去,以保持父子协程的取消语义一致。 + /// + public CancellationToken CancellationToken; + /// /// 协程枚举器,包含协程的执行逻辑 /// 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..752310f8 --- /dev/null +++ b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs @@ -0,0 +1,73 @@ +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)); + } + + /// + /// 验证绝对时间源在回拨时仍保持单调,不会把 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.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..06cd5cbd 100644 --- a/GFramework.Godot/Coroutine/GodotTimeSource.cs +++ b/GFramework.Godot/Coroutine/GodotTimeSource.cs @@ -3,40 +3,80 @@ 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; + } + + // 对绝对时间源做单调钳制,避免 provider 回拨后把 CurrentTime 也拉回去。 + var nextTime = Math.Max(value, _lastAbsoluteTime); + DeltaTime = nextTime - _lastAbsoluteTime; + _lastAbsoluteTime = nextTime; + CurrentTime = nextTime; + 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..d8913a7e 100644 --- a/GFramework.Godot/Coroutine/Timing.cs +++ b/GFramework.Godot/Coroutine/Timing.cs @@ -3,73 +3,78 @@ using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Coroutine; using GFramework.Core.Coroutine.Instructions; using GFramework.Godot.Extensions; -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 +88,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 +160,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 +197,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 +239,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 +321,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 +358,124 @@ 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; + } + + if (!GetScheduler(segment).IsCoroutineAlive(handle)) + { + return handle; + } + + RegisterOwnedCoroutine(owner, handle); + return handle; } #endregion @@ -355,60 +483,109 @@ 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) + { + var count = 0; + foreach (var timing in EnumerateActiveInstances()) + { + count += timing.KillOwnedCoroutinesOnInstance(owner); + } + + return count; + } + + /// + /// 终止所有具有指定标签的协程。 + /// + /// 协程标签。 + /// 被终止的协程数量。 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) + { + var count = 0; + foreach (var timing in EnumerateActiveInstances()) + { + count += timing.GetOwnedCoroutineCountOnInstance(owner); + } + + return count; + } + + /// + /// 在当前实例上暂停协程。 + /// + /// 协程句柄。 + /// 如果成功暂停则返回 private bool PauseOnInstance(CoroutineHandle handle) { return ProcessScheduler.Pause(handle) @@ -418,11 +595,10 @@ public partial class Timing : Node } /// - /// 在当前实例上恢复协程 - /// 尝试在所有调度器中查找并恢复指定协程 + /// 在当前实例上恢复协程。 /// - /// 协程句柄 - /// 是否成功恢复 + /// 协程句柄。 + /// 如果成功恢复则返回 private bool ResumeOnInstance(CoroutineHandle handle) { return ProcessScheduler.Resume(handle) @@ -432,11 +608,10 @@ public partial class Timing : Node } /// - /// 在当前实例上终止协程 - /// 尝试在所有调度器中查找并终止指定协程 + /// 在当前实例上终止协程。 /// - /// 协程句柄 - /// 是否成功终止 + /// 协程句柄。 + /// 如果成功终止则返回 private bool KillOnInstance(CoroutineHandle handle) { return ProcessScheduler.Kill(handle) @@ -446,11 +621,10 @@ public partial class Timing : Node } /// - /// 在当前实例上根据标签终止协程 - /// 在所有调度器中查找并终止具有指定标签的协程 + /// 在当前实例上根据标签终止协程。 /// - /// 协程标签 - /// 被终止的协程数量 + /// 协程标签。 + /// 被终止的协程数量。 private int KillByTagOnInstance(string tag) { var count = 0; @@ -462,10 +636,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 +690,172 @@ 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; } + /// + /// 枚举所有当前已注册的 Timing 实例。 + /// + /// 活跃 Timing 实例序列。 + private static IEnumerable EnumerateActiveInstances() + { + return ActiveInstances.Where(static timing => timing is not null).Select(static timing => timing!); + } /// - /// 检查节点是否处于有效状态 + /// 检查节点是否处于有效状态。 /// - /// 要检查的节点 - /// 如果节点存在且有效则返回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 +863,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 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)