docs(coroutine): 更新协程系统文档

- 重构 Core 协程系统文档,优化概述和核心概念说明
- 新增 Godot 协程系统集成文档
- 添加协程系统使用教程
- 更新等待指令说明,包括时间、条件、Task 和事件等待
- 补充协程控制、快照查询和生命周期管理相关内容
- 修正代码示例和 API 使用说明
This commit is contained in:
GeWuYou 2026-04-05 15:06:53 +08:00
parent 1c41c57d72
commit 03346fbfe7
3 changed files with 332 additions and 1074 deletions

View File

@ -1,61 +1,95 @@
---
title: 协程系统
description: 协程系统提供基于 IEnumerator<IYieldInstruction>调度、等待和组合能力可与事件、Task、命令与查询集成
description: 基于 IEnumerator<IYieldInstruction>协程调度系统支持时间等待、阶段等待、Task 桥接、事件等待与运行时快照查询
---
# 协程系统
## 概述
GFramework 的 Core 协程系统基于 `IEnumerator<IYieldInstruction>` 构建,通过 `CoroutineScheduler`
统一推进协程执行。它适合处理分帧逻辑、时间等待、条件等待、Task 桥接,以及事件驱动的异步流程。
`GFramework.Core.Coroutine` 提供一个宿主无关的协程内核。它围绕 `CoroutineScheduler` 工作,统一处理:
协程系统主要由以下部分组成:
- `IEnumerator<IYieldInstruction>` 形式的协程推进
- 时间等待、条件等待、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();
var handle = scheduler.Run(
LoadResources(),
tag: "loading",
group: "bootstrap",
cancellationToken: cts.Token);
if (scheduler.IsCoroutineAlive(handle))
{
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<IYieldInstruction> 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<IYieldInstruction> 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<IYieldInstruction>`,再传给 `scheduler.Run(...)`
`Sequence(...)` 或其他只接受协程枚举器的 API。
- `StartTaskAsCoroutine()` 适合已经持有 `CoroutineScheduler`,并希望把 `Task` 直接作为一个顶层协程启动的场景。
### 等待事件
```csharp
@ -188,236 +158,52 @@ using GFramework.Core.Coroutine.Instructions;
public IEnumerator<IYieldInstruction> WaitForEventExample(IEventBus eventBus)
{
using var waitEvent = new WaitForEvent<PlayerDiedEvent>(eventBus);
yield return waitEvent;
var eventData = waitEvent.EventData;
Console.WriteLine($"玩家 {eventData!.PlayerId} 死亡");
}
```
为事件等待附加超时:
```csharp
public IEnumerator<IYieldInstruction> WaitForEventWithTimeoutExample(IEventBus eventBus)
{
using var waitEvent = new WaitForEvent<PlayerJoinedEvent>(eventBus);
var timeoutWait = new WaitForEventWithTimeout<PlayerJoinedEvent>(waitEvent, 5.0f);
yield return timeoutWait;
if (timeoutWait.IsTimeout)
Console.WriteLine("等待超时");
else
Console.WriteLine($"玩家加入: {timeoutWait.EventData!.PlayerName}");
}
```
等待两个事件中的任意一个:
```csharp
public IEnumerator<IYieldInstruction> WaitForEitherEvent(IEventBus eventBus)
{
using var wait = new WaitForMultipleEvents<PlayerReadyEvent, PlayerQuitEvent>(eventBus);
using var wait = new WaitForEvent<PlayerJoinedEvent>(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<IYieldInstruction> ParentCoroutine()
{
Console.WriteLine("父协程开始");
yield return new WaitForCoroutine(ChildCoroutine());
Console.WriteLine("子协程完成");
}
private IEnumerator<IYieldInstruction> ChildCoroutine()
{
yield return CoroutineHelper.WaitForSeconds(1.0);
Console.WriteLine("子协程执行");
yield return new Delay(1.0);
}
```
等待多个句柄全部完成:
如果需要等待多个顶层协程句柄,可以结合 `WaitForAllCoroutines``ParallelCoroutines(...)` 使用。
```csharp
public IEnumerator<IYieldInstruction> WaitForMultipleCoroutines(CoroutineScheduler scheduler)
{
var handles = new List<CoroutineHandle>
{
scheduler.Run(LoadTexture()),
scheduler.Run(LoadAudio()),
scheduler.Run(LoadModel())
};
## 建议
yield return new WaitForAllCoroutines(scheduler, handles);
Console.WriteLine("所有资源加载完成");
}
```
### 进度等待
```csharp
public IEnumerator<IYieldInstruction> 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<T>` 包装成等待指令
- `ToCoroutineEnumerator()`:把 `Task` / `Task<T>` 转成协程枚举器
- `StartTaskAsCoroutine()`:直接通过调度器启动 Task 协程
### 命令、查询与 Mediator 扩展
这些扩展都定义在 `GFramework.Core.Coroutine.Extensions` 命名空间中。
### 命令协程
```csharp
using GFramework.Core.Coroutine.Extensions;
public IEnumerator<IYieldInstruction> ExecuteCommand(IContextAware contextAware)
{
yield return contextAware.SendCommandCoroutineWithErrorHandler(
new LoadSceneCommand(),
ex => Console.WriteLine(ex.Message));
}
```
如果命令执行后需要等待事件:
```csharp
public IEnumerator<IYieldInstruction> ExecuteCommandAndWaitEvent(IContextAware contextAware)
{
yield return contextAware.SendCommandAndWaitEventCoroutine<LoadSceneCommand, SceneLoadedEvent>(
new LoadSceneCommand(),
evt => Console.WriteLine($"场景加载完成: {evt.SceneName}"),
timeout: 5.0f);
}
```
### 查询协程
`SendQueryCoroutine` 会同步执行查询,并通过回调返回结果:
```csharp
public IEnumerator<IYieldInstruction> QueryPlayer(IContextAware contextAware)
{
yield return contextAware.SendQueryCoroutine<GetPlayerDataQuery, PlayerData>(
new GetPlayerDataQuery { PlayerId = 1 },
playerData => Console.WriteLine($"玩家名称: {playerData.Name}"));
}
```
### Mediator 协程
如果项目使用 `Mediator.IMediator`,还可以使用 `MediatorCoroutineExtensions`
```csharp
public IEnumerator<IYieldInstruction> 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<T>` 检查任务异常。
## 常见问题
### 协程什么时候执行?
协程在调度器的 `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()`

View File

@ -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<IYieldInstruction>`
## 启动协程
## 主要能力
- 在 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<IYieldInstruction> 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()
{
LongRunningTask()
.CancelWith(this)
.RunCoroutine();
}
private IEnumerator<IYieldInstruction> 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<IYieldInstruction> WaitForGameEvent(IEventBus eventBus)
{
using var wait = new WaitForEvent<PlayerSpawnedEvent>(eventBus);
yield return wait;
var evt = wait.EventData;
}
```
如需为事件等待附加超时控制,可结合 `WaitForEventWithTimeout<TEvent>`
## 协程控制
协程启动后会返回 `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<TResponse>(...)`
- `RunQueryCoroutine<TResponse>(...)`
- `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`、节点归属和标签控制一起使用。

View File

@ -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<IYieldInstruction>` 协程
- 区分缩放时间、真实时间与阶段等待
- 使用句柄、取消令牌和快照查询控制协程
- 在 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);
}
public void Start()
{
_scheduler.Run(MyFirstCoroutine(), tag: "tutorial");
}
public void Tick()
{
_scheduler.Update();
}
/// <summary>
/// 第一个协程示例
/// </summary>
private IEnumerator<IYieldInstruction> MyFirstCoroutine()
{
Console.WriteLine("协程开始执行");
Console.WriteLine("协程开始");
// 等待 1 秒
yield return CoroutineHelper.WaitForSeconds(1.0);
yield return new Delay(1.0);
Console.WriteLine("1 秒后");
Console.WriteLine("1 秒后执行");
yield return new WaitOneFrame();
Console.WriteLine("下一帧");
// 等待 1 帧
yield return CoroutineHelper.WaitForOneFrame();
Console.WriteLine("下一帧执行");
// 等待 5 帧
yield return CoroutineHelper.WaitForFrames(5);
Console.WriteLine("5 帧后执行");
}
yield return new WaitForFrames(3);
Console.WriteLine("3 帧后");
}
}
```
**代码说明**
关键点:
- 协程方法返回 `IEnumerator<IYieldInstruction>`
- 使用 `yield return` 返回等待指令
- `this.StartCoroutine()` 扩展方法启动协程
- `WaitForSeconds` 等待指定秒数
- `WaitForOneFrame` 等待一帧
- `WaitForFrames` 等待多帧
- 协程返回类型必须是 `IEnumerator<IYieldInstruction>`
- 调度器不会自动运行,你必须在宿主主循环中调用 `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
Console.WriteLine(snapshot.State);
Console.WriteLine(snapshot.WaitingInstructionType);
}
```
## 步骤 3区分时间等待
```csharp
private IEnumerator<IYieldInstruction> CooldownCoroutine()
{
// 当前生命值
public BindableProperty<int> Health { get; } = new(100);
// 使用宿主默认时间
yield return new Delay(2.0);
// 最大生命值
public BindableProperty<int> MaxHealth { get; } = new(100);
// 使用真实时间,需要调度器提供 realtimeTimeSource
yield return new WaitForSecondsRealtime(2.0);
}
```
// 是否启用自动恢复
public BindableProperty<bool> AutoRegenEnabled { get; } = new(true);
建议:
private CoroutineHandle? _regenHandle;
- 普通游戏逻辑优先使用 `Delay`
- 暂停菜单、真实倒计时、网络超时等场景使用 `WaitForSecondsRealtime`
protected override void OnInit()
## 步骤 4使用阶段等待
只有宿主为调度器提供了匹配阶段时,阶段等待才会真实生效:
```csharp
var fixedScheduler = new CoroutineScheduler(
fixedTimeSource,
executionStage: CoroutineExecutionStage.FixedUpdate);
private IEnumerator<IYieldInstruction> PhysicsCoroutine()
{
// 启动生命值恢复协程
StartHealthRegeneration();
yield return new WaitForFixedUpdate();
Console.WriteLine("下一次固定步到达");
}
```
同理,`WaitForEndOfFrame` 需要运行在 `CoroutineExecutionStage.EndOfFrame` 的调度器上。
## 步骤 5等待 Task
```csharp
using GFramework.Core.Coroutine.Extensions;
private IEnumerator<IYieldInstruction> 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;
public partial class DemoNode : Node
{
public override void _Ready()
{
// 推荐:节点作为所有者运行协程
this.RunCoroutine(BlinkCoroutine(), Segment.ProcessIgnorePause, tag: "blink");
}
/// <summary>
/// 启动生命值恢复
/// </summary>
public void StartHealthRegeneration()
{
// 如果已经在运行,先停止
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
}
// 启动新的恢复协程
_regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
}
/// <summary>
/// 停止生命值恢复
/// </summary>
public void StopHealthRegeneration()
{
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
_regenHandle = null;
}
}
/// <summary>
/// 生命值恢复协程
/// </summary>
private IEnumerator<IYieldInstruction> HealthRegenerationCoroutine()
private IEnumerator<IYieldInstruction> BlinkCoroutine()
{
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}");
}
}
Visible = !Visible;
yield return new WaitForSecondsRealtime(0.5);
}
}
}
```
**代码说明**
`DemoNode` 退出场景树时,上面的协程会被自动终止。
- 使用 `while (true)` 创建无限循环协程
- 保存协程句柄以便后续控制
- 使用 `StopCoroutine` 停止协程
- 协程中可以访问类成员变量
## 步骤 3实现技能冷却系统
接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
如果你需要绑定多个节点,可以继续使用:
```csharp
using GFramework.Core.System;
using System.Collections.Generic;
namespace MyGame.Systems
{
public class SkillSystem : AbstractSystem
{
// 技能冷却状态
private readonly Dictionary<string, bool> _skillCooldowns = new();
/// <summary>
/// 使用技能
/// </summary>
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;
}
/// <summary>
/// 技能冷却协程
/// </summary>
private IEnumerator<IYieldInstruction> SkillCooldownCoroutine(string skillName, double cooldownTime)
{
// 标记为冷却中
_skillCooldowns[skillName] = true;
Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
// 等待冷却时间
yield return CoroutineHelper.WaitForSeconds(cooldownTime);
// 冷却结束
_skillCooldowns[skillName] = false;
Console.WriteLine($"技能 {skillName} 冷却完成");
}
/// <summary>
/// 带进度显示的技能冷却
/// </summary>
private IEnumerator<IYieldInstruction> 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} 冷却完成");
}
}
}
BlinkCoroutine()
.CancelWith(this, anotherNode)
.RunCoroutine();
```
**代码说明**
- 使用字典管理多个技能的冷却状态
- 每个技能使用独立的协程管理冷却
- `WaitForProgress` 可以在等待期间执行回调
- 协程结束后自动清理冷却状态
## 步骤 4等待事件触发
实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。
```csharp
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Coroutine.Instructions;
namespace MyGame.Systems
{
// 任务完成事件
public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
public class QuestSystem : AbstractSystem
{
/// <summary>
/// 开始任务并等待完成
/// </summary>
public void StartQuest(int questId, string questName)
{
this.StartCoroutine(QuestCoroutine(questId, questName));
}
/// <summary>
/// 任务协程
/// </summary>
private IEnumerator<IYieldInstruction> QuestCoroutine(int questId, string questName)
{
Console.WriteLine($"任务开始: {questName}");
// 获取事件总线
var eventBus = this.GetService<IEventBus>();
// 等待任务完成事件
var waitEvent = new WaitForEvent<QuestCompletedEvent>(
eventBus,
evt => evt.QuestId == questId // 过滤条件
);
yield return waitEvent;
// 获取事件数据
var completedEvent = waitEvent.EventData;
Console.WriteLine($"任务完成: {completedEvent.QuestName}");
// 发放奖励
GiveReward(questId);
}
/// <summary>
/// 带超时的任务
/// </summary>
private IEnumerator<IYieldInstruction> TimedQuestCoroutine(
int questId,
string questName,
double timeLimit)
{
Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
var eventBus = this.GetService<IEventBus>();
// 等待事件,带超时
var waitEvent = new WaitForEventWithTimeout<QuestCompletedEvent>(
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} 的奖励");
}
}
}
```
**代码说明**
- `WaitForEvent` 等待特定事件触发
- 可以使用 `predicate` 参数过滤事件
- `WaitForEventWithTimeout` 支持超时机制
- 通过 `EventData` 属性获取事件数据
## 步骤 5协程组合与嵌套
实现一个复杂的游戏流程,展示如何组合多个协程。
```csharp
namespace MyGame.Systems
{
public class GameFlowSystem : AbstractSystem
{
/// <summary>
/// 游戏开始流程
/// </summary>
public void StartGame()
{
this.StartCoroutine(GameStartSequence());
}
/// <summary>
/// 游戏开始序列
/// </summary>
private IEnumerator<IYieldInstruction> GameStartSequence()
{
Console.WriteLine("=== 游戏开始 ===");
// 1. 显示标题
yield return ShowTitle();
// 2. 加载资源
yield return LoadResources();
// 3. 初始化玩家
yield return InitializePlayer();
// 4. 播放开场动画
yield return PlayOpeningAnimation();
Console.WriteLine("=== 游戏准备完成 ===");
}
/// <summary>
/// 显示标题
/// </summary>
private IEnumerator<IYieldInstruction> ShowTitle()
{
Console.WriteLine("显示游戏标题...");
yield return CoroutineHelper.WaitForSeconds(2.0);
Console.WriteLine("标题显示完成");
}
/// <summary>
/// 加载资源
/// </summary>
private IEnumerator<IYieldInstruction> 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<IYieldInstruction> LoadTexturesCoroutine()
{
Console.WriteLine(" 加载纹理...");
yield return CoroutineHelper.WaitForSeconds(1.0);
Console.WriteLine(" 纹理加载完成");
}
private IEnumerator<IYieldInstruction> LoadAudioCoroutine()
{
Console.WriteLine(" 加载音频...");
yield return CoroutineHelper.WaitForSeconds(1.5);
Console.WriteLine(" 音频加载完成");
}
private IEnumerator<IYieldInstruction> LoadModelsCoroutine()
{
Console.WriteLine(" 加载模型...");
yield return CoroutineHelper.WaitForSeconds(0.8);
Console.WriteLine(" 模型加载完成");
}
private IEnumerator<IYieldInstruction> InitializePlayer()
{
Console.WriteLine("初始化玩家...");
yield return CoroutineHelper.WaitForSeconds(0.5);
Console.WriteLine("玩家初始化完成");
}
private IEnumerator<IYieldInstruction> PlayOpeningAnimation()
{
Console.WriteLine("播放开场动画...");
yield return CoroutineHelper.WaitForSeconds(3.0);
Console.WriteLine("开场动画播放完成");
}
/// <summary>
/// 获取协程调度器
/// </summary>
private CoroutineScheduler GetCoroutineScheduler()
{
// 从架构服务中获取
return this.GetService<CoroutineScheduler>();
}
}
}
```
**代码说明**
- 使用 `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>();
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>();
questSystem.StartQuest(1, "击败史莱姆");
// 模拟任务完成
await Task.Delay(2000);
var eventBus = architecture.GetService<IEventBus>();
eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
// 测试游戏流程
var gameFlowSystem = architecture.GetSystem<GameFlowSystem>();
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)