Add component coroutine support#172
Open
shrinktofit wants to merge 1 commit into
Open
Conversation
Code Size Check Report
Interface Check Report! WARNING this pull request has changed these public interfaces:
@@ -25686,8 +25686,27 @@
* @deprecated since v3.5.0, this is an engine private interface that will be removed in the future.
*/
_instantiate(cloned?: Component): Component | undefined;
/**
+ * @en Starts a coroutine owned by this component.
+ * @zh 启动一个由当前组件持有的协程。
+ * @param coroutine The coroutine iterator to start.
+ * @param opts Optional start options.
+ * @returns An opaque coroutine handle that can be passed to stopCoroutine.
+ */
+ protected startCoroutine(coroutine: CoroutineIterator, opts?: StartCoroutineOptions): Coroutine;
+ /**
+ * @en Stops a coroutine.
+ * @zh 停止一个协程。
+ * @param coroutine The coroutine handle returned by startCoroutine.
+ */
+ protected stopCoroutine(coroutine: Coroutine): void;
+ /**
+ * @en Stops all coroutines owned by this component.
+ * @zh 停止当前组件持有的所有协程。
+ */
+ protected stopAllCoroutines(): void;
+ /**
* @en
* Use Scheduler system to schedule a custom task.<br/>
* If the task is already scheduled, then the interval parameter will be updated without scheduling it again.
* @zh
@@ -26999,8 +27018,81 @@
*/
activate(scene: Scene): void;
}
/**
+ * Resume the coroutine on the next coroutine update.
+ */
+ export function nextFrame(): CoroutineInstruction;
+ /**
+ * Resume the coroutine after at least the given duration.
+ */
+ export function waitFor(seconds: number): CoroutineInstruction;
+ /**
+ * Resume the coroutine when the predicate becomes true.
+ */
+ export function waitUntil(predicate: CoroutinePredicate): CoroutineInstruction;
+ /**
+ * Resume the coroutine when the predicate becomes false.
+ */
+ export function waitWhile(predicate: CoroutinePredicate): CoroutineInstruction;
+ export function stopCoroutine(coroutine: Coroutine): void;
+ /**
+ * Frame data passed to a coroutine when it resumes.
+ */
+ export interface CoroutineFrame {
+ /**
+ * Time elapsed since the previous coroutine update, in seconds.
+ */
+ readonly deltaTime: number;
+ /**
+ * Time elapsed since this coroutine started running, in seconds.
+ */
+ readonly elapsedTime: number;
+ /**
+ * Number of coroutine update frames since the owner component started running coroutines.
+ */
+ readonly frame: number;
+ }
+ /**
+ * Handle returned by `Component.startCoroutine`.
+ */
+ export interface Coroutine {
+ readonly [__private._cocos_scene_graph_component_coroutine__CoroutineBrand]: never;
+ }
+ /**
+ * Yield instruction returned by coroutine wait helpers.
+ */
+ export interface CoroutineInstruction {
+ readonly [__private._cocos_scene_graph_component_coroutine__CoroutineInstructionBrand]: never;
+ }
+ /**
+ * Iterator shape accepted by `Component.startCoroutine`.
+ */
+ export type CoroutineIterator = Generator<__private._cocos_scene_graph_component_coroutine__CoroutineYield, void, CoroutineFrame>;
+ /**
+ * Predicate evaluated with the current coroutine frame.
+ */
+ export type CoroutinePredicate = (frame: CoroutineFrame) => boolean;
+ /**
+ * Options used when starting a component coroutine.
+ */
+ export interface StartCoroutineOptions {
+ /**
+ * Abort signal that stops the coroutine when aborted.
+ */
+ readonly signal?: __private._cocos_scene_graph_component_coroutine__CoroutineAbortSignal;
+ }
+ export class CoroutineRunner {
+ start(coroutine: CoroutineIterator, firstResumeFrame: number, opts?: StartCoroutineOptions): Coroutine;
+ update(deltaTime: number, directorFrame: number): void;
+ stop(handle: Coroutine): void;
+ stopAll(): void;
+ get frame(): number;
+ get deltaTime(): number;
+ get directorFrame(): number;
+ get empty(): boolean;
+ }
+ /**
* @internal
* @deprecated since v3.5
*/
export class PrivateNode extends Node {
@@ -67145,9 +67237,9 @@
* @deprecated since v3.4
*/
static markOpacityTree(node: any, isDirty?: boolean): void;
}
- export type _cocos_scene_graph_component_scheduler__InvokeFunc = (...args: unknown[]) => void;
+ export type _cocos_scene_graph_component_scheduler__InvokeFunc = (iterator: any, dt?: any, directorFrame?: any) => void;
function _cocos_scene_graph_component_scheduler__stableRemoveInactive(iterator: any, flagToClear: any): void;
export class _cocos_scene_graph_component_scheduler__LifeCycleInvoker {
static stableRemoveInactive: typeof _cocos_scene_graph_component_scheduler__stableRemoveInactive;
protected _zero: js.array.MutableForwardIterator<any>;
@@ -67174,8 +67266,16 @@
onLoad: _cocos_scene_graph_component_scheduler__OneOffInvoker;
onEnable: _cocos_scene_graph_component_scheduler__OneOffInvoker;
}
export type _cocos_asset_assets_material__MaterialPropertyFull = renderer.MaterialProperty | _cocos_asset_assets_texture_base__TextureBase | gfx.Texture | null;
+ export const _cocos_scene_graph_component_coroutine__CoroutineBrand: unique symbol;
+ export const _cocos_scene_graph_component_coroutine__CoroutineInstructionBrand: unique symbol;
+ export type _cocos_scene_graph_component_coroutine__CoroutineYield = CoroutineInstruction | null | undefined;
+ export interface _cocos_scene_graph_component_coroutine__CoroutineAbortSignal {
+ readonly aborted: boolean;
+ addEventListener?: (type: "abort", listener: () => void) => void;
+ removeEventListener?: (type: "abort", listener: () => void) => void;
+ }
export enum _cocos_misc_camera_component__CameraEvent {
TARGET_TEXTURE_CHANGE = "tex-change"
}
export const _cocos_misc_camera_component__ProjectionType: typeof renderer.scene.CameraProjection;
@@ -67212,9 +67312,9 @@
export class _cocos_scene_graph_component_scheduler__ReusableInvoker extends _cocos_scene_graph_component_scheduler__LifeCycleInvoker {
constructor(invokeFunc: _cocos_scene_graph_component_scheduler__InvokeFunc);
add(comp: Component): void;
remove(comp: Component): void;
- invoke(dt: number): void;
+ invoke(dt: number, directorFrame?: number): void;
}
/**
* @en The Manager for Component's life-cycle methods.
* It collaborates with [[NodeActivator]] to schedule and invoke life cycle methods for components
@@ -67236,8 +67336,13 @@
* @en The invoker of `lateUpdate` callback
* @zh `lateUpdate` 回调的调度器
*/
lateUpdateInvoker: _cocos_scene_graph_component_scheduler__ReusableInvoker;
+ /**
+ * @en The invoker of component coroutine callbacks
+ * @zh 组件协程回调的调度器
+ */
+ coroutineInvoker: _cocos_scene_graph_component_scheduler__ReusableInvoker;
constructor();
/**
* @en Cancel all future callbacks, including `start`, `update` and `lateUpdate`
* @zh 取消所有未来的函数调度,包括 `start`,`update` 和 `lateUpdate`
@@ -67275,8 +67380,15 @@
* @param dt @en Time passed after the last frame in seconds @zh 距离上一帧的时间,以秒计算
*/
updatePhase(dt: number): void;
/**
+ * @en Process coroutine phase for registered components
+ * @zh 为当前注册的组件执行协程阶段任务
+ * @param dt @en Time passed after the last frame in seconds @zh 距离上一帧的时间,以秒计算
+ * @param directorFrame @en Current director frame @zh 当前导演帧号
+ */
+ coroutineUpdatePhase(dt: number, directorFrame: number): void;
+ /**
* @en Process late update phase for registered components
* @zh 为当前注册的组件执行 late update 阶段任务
* @param dt @en Time passed after the last frame in seconds @zh 距离上一帧的时间,以秒计算
*/
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re: #
Changelog
Motivation
Many gameplay and UI component flows are naturally sequential, but they often need to span multiple frames: wait for an animation to finish, delay for a short amount of time, poll for a condition, then continue with the next step. Without a component-owned coroutine mechanism, those flows have to be rewritten as explicit state machines spread across component fields and
update().For example, a simple enemy spawn warning flow can become bookkeeping-heavy:
The real intent is linear: show the warning, wait until it is ready, wait half a second, then spawn. Coroutines let component authors express that intent directly while still running on the engine frame loop:
This PR adds that missing component-level control-flow primitive with Unity-style timing: coroutine continuations resume after component
update()callbacks and before later frame phases.Usage
Components can start a generator-based coroutine directly:
startCoroutine()returns aCoroutinehandle. The handle can be passed tostopCoroutine(handle), whilestopAllCoroutines()stops every coroutine owned by the component. Supported waits include:Details
startCoroutine()runs synchronously until the first yield, so any work before that first yield happens immediately in the caller's stack.yield null,yield undefined, andyield nextFrame()resume during the coroutine phase no earlier than the next frame.update()callbacks, before director systems update, and before componentlateUpdate()callbacks.update()or during another coroutine continuation are queued for a later coroutine phase and do not resume in the same frame.waitFor(seconds)resumes after enough framedthas accumulated.waitUntil(predicate)resumes when the predicate becomes true.waitWhile(predicate)resumes when the predicate becomes false.component.enabled = falsedoes not pause or stop coroutines owned by that component.node.active = falsestops coroutines owned by affected components. Component destruction also stops owned coroutines.stopCoroutine(handle),stopAllCoroutines(), and an aborted signal stop coroutines and run generator cleanup, includingfinallyblocks.stopAllCoroutines()is safe to call from inside a coroutine continuation or cleanup.Implementation
cocos/scene-graph/component-coroutine.tswith public coroutine types and helper instructions.cc.Componentwith protectedstartCoroutine(),stopCoroutine(), andstopAllCoroutines()methods.update()and before director systems update.Tests
Added
tests/scene-graph/component-coroutine.test.tscovering the behavior above, including lifecycle, ordering, waits, generator cleanup, and mutation safety.Verified with:
npx jest tests/scene-graph/component-coroutine.test.ts --runInBandnpm run testContinuous Integration
This pull request:
Compatibility Check
This pull request: