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)