diff --git a/docs/en/animation/animator.mdx b/docs/en/animation/animator.mdx index d4cf4f3d8d..a253d0cd98 100644 --- a/docs/en/animation/animator.mdx +++ b/docs/en/animation/animator.mdx @@ -113,11 +113,20 @@ animator.speed = 0; animator.speed = 1; ``` -If you only want to pause a specific `AnimatorState` , you can do so by setting its speed to 0. +If you only want to pause a specific `AnimatorState` on this Animator instance, set the per-instance speed to 0. ```typescript const state = animator.findAnimatorState("xxx"); +if (!state) { + // State not found in any layer + return; +} + +// Pause only this Animator instance's playback of the state. state.speed = 0; + +// To resume later, write any non-zero value: +// state.speed = 1; ``` ### Transition to Specified Animation State @@ -130,26 +139,34 @@ animator.crossFade("OtherStateName", 0.3); ### Get Current Playing Animation State -You can use the [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) method to get the currently playing `AnimatorState` . The parameter is the index `layerIndex` of the `AnimatorControllerLayer` where the `AnimatorState` is located. For details, see the [API documentation](/apis/core/#Animator-getCurrentAnimatorState). After obtaining it, you can set the properties of the `AnimatorState` , such as changing the default loop playback to play once. +[getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) returns the playing `AnimatorStateInstance` on a given layer, or `null` when the layer is missing or nothing is playing. ```typescript -const currentState = animator.getCurrentAnimatorState(0); -// Play once -currentState.wrapMode = WrapMode.Once; -// Loop playback -currentState.wrapMode = WrapMode.Loop; +const current = animator.getCurrentAnimatorState(0); +if (current) { + current.speed = 0.5; +} ``` ### Get Animation State -You can use the [findAnimatorState](/apis/core/#Animator-findAnimatorState) method to get the specified `AnimatorState` . After obtaining it, you can set the properties of the `AnimatorState` , such as changing the default loop playback to play once. +[findAnimatorState](/apis/core/#Animator-findAnimatorState) returns the per-Animator `AnimatorStateInstance` (`AnimatorStateInstance | null`). Writes only affect this Animator; the shared `AnimatorState` on the controller is untouched. Same pattern as `Renderer.getInstanceMaterial`. + +- `speed`, `wrapMode` are per-instance overrideable; unwritten reads return the shared default. +- `name`, `clip`, `clipStartTime`, `clipEndTime` read through to the shared asset. + +To mutate the shared asset (broadcasts to every Animator), go through the controller path: ```typescript const state = animator.findAnimatorState("xxx"); -// Play once +if (!state) return; + +// Per-instance overrides. +state.speed = 0.5; state.wrapMode = WrapMode.Once; -// Loop playback -state.wrapMode = WrapMode.Loop; + +// Broadcast to every Animator using this controller. +animator.animatorController.layers[0].stateMachine.findStateByName("xxx").clip = otherClip; ``` ### Animation Culling diff --git a/docs/zh/animation/animator.mdx b/docs/zh/animation/animator.mdx index 0f95eb4550..5246416625 100644 --- a/docs/zh/animation/animator.mdx +++ b/docs/zh/animation/animator.mdx @@ -117,11 +117,20 @@ animator.speed = 1; ``` -如果你只想针对某一个 `动画状态` 进行暂停,可以通过将它的速度设置为 0 来实现。 +如果你只想针对当前 `Animator` 上的某个 `动画状态` 进行暂停,将它的 per-instance 速度设为 0 即可。 ```typescript const state = animator.findAnimatorState("xxx"); +if (!state) { + // 任何一个动画层都没有该状态 + return; +} + +// 仅暂停当前 Animator 实例的该状态播放。 state.speed = 0; + +// 想要恢复时写回任意非零速度即可: +// state.speed = 1; ``` ### 过渡指定动画状态 @@ -135,26 +144,34 @@ animator.crossFade("OtherStateName", 0.3); ### 获取当前在播放的动画状态 -你可以使用 [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState)  方法来获取当前正在播放的 `动画状态`。参数为 `动画状态` 所在 `动画层` 的序号`layerIndex`, 详见[API 文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置 `动画状态` 的属性,比如将默认的循环播放改为一次。 +[getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) 返回指定层当前播放的 `AnimatorStateInstance`,层不存在或未在播放时返回 `null`。 ```typescript -const currentState = animator.getCurrentAnimatorState(0); -// 播放一次 -currentState.wrapMode = WrapMode.Once; -// 循环播放 -currentState.wrapMode = WrapMode.Loop; +const current = animator.getCurrentAnimatorState(0); +if (current) { + current.speed = 0.5; +} ``` ### 获取动画状态 -你可以使用 [findAnimatorState](/apis/core/#Animator-findAnimatorState)  方法来获取指定名称的 `动画状态` 。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。 +[findAnimatorState](/apis/core/#Animator-findAnimatorState) 返回当前 `Animator` 独有的 `AnimatorStateInstance`(`AnimatorStateInstance | null`)。对它的写入只影响当前 Animator,共享 `AnimatorState` 资产不受影响。模式与 `Renderer.getInstanceMaterial` 一致。 + +- `speed`、`wrapMode` 可逐实例覆盖;未写入前透传到共享默认值。 +- `name` / `clip` / `clipStartTime` / `clipEndTime` 从共享资产转发。 + +要修改共享资产(广播到所有使用该控制器的 Animator),通过控制器路径访问: ```typescript const state = animator.findAnimatorState("xxx"); -// 播放一次 +if (!state) return; + +// 只影响当前 Animator +state.speed = 0.5; state.wrapMode = WrapMode.Once; -// 循环播放 -state.wrapMode = WrapMode.Loop; + +// 广播到所有使用该控制器的 Animator +animator.animatorController.layers[0].stateMachine.findStateByName("xxx").clip = otherClip; ``` ### 动画裁剪 diff --git a/e2e/case/animator-additive.ts b/e2e/case/animator-additive.ts index b30f2bf983..462f8f4f51 100644 --- a/e2e/case/animator-additive.ts +++ b/e2e/case/animator-additive.ts @@ -56,7 +56,11 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const additivePoseNames = animations.filter((clip) => clip.name.includes("pose")).map((clip) => clip.name); additivePoseNames.forEach((name) => { - const clip = animator.findAnimatorState(name).clip; + const state = animator.findAnimatorState(name); + if (!state) { + throw new Error(`Animator state not found: ${name}`); + } + const clip = state.clip; const newState = animatorStateMachine.addState(name); newState.clipStartTime = 1; newState.clip = clip; diff --git a/e2e/case/animator-event.ts b/e2e/case/animator-event.ts index 430b260dc2..eb83a74734 100644 --- a/e2e/case/animator-event.ts +++ b/e2e/case/animator-event.ts @@ -53,6 +53,9 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const animator = defaultSceneRoot.getComponent(Animator); const state = animator.findAnimatorState("walk"); + if (!state) { + throw new Error("Animator state not found: walk"); + } const clip = state.clip; const event0 = new AnimationEvent(); diff --git a/e2e/case/animator-play-backwards.ts b/e2e/case/animator-play-backwards.ts index 526d21b9ca..936a370048 100644 --- a/e2e/case/animator-play-backwards.ts +++ b/e2e/case/animator-play-backwards.ts @@ -33,7 +33,11 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const { defaultSceneRoot } = gltfResource; rootEntity.addChild(defaultSceneRoot); const animator = defaultSceneRoot.getComponent(Animator); - animator.findAnimatorState("walk").speed = -1; + const walkState = animator.findAnimatorState("walk"); + if (!walkState) { + throw new Error("Animator state not found: walk"); + } + walkState.speed = -1; animator.play("walk"); updateForE2E(engine); diff --git a/e2e/case/animator-stateMachine.ts b/e2e/case/animator-stateMachine.ts index 08fb9e6d39..4f449d8487 100644 --- a/e2e/case/animator-stateMachine.ts +++ b/e2e/case/animator-stateMachine.ts @@ -54,9 +54,12 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { const animator = defaultSceneRoot.getComponent(Animator)!; animator.animatorController.addParameter("playerSpeed", 1); const stateMachine = animator.animatorController.layers[0].stateMachine; - const idleState = animator.findAnimatorState("idle"); - const walkState = animator.findAnimatorState("walk"); - const runState = animator.findAnimatorState("run"); + const idleState = stateMachine.findStateByName("idle"); + const walkState = stateMachine.findStateByName("walk"); + const runState = stateMachine.findStateByName("run"); + if (!idleState || !walkState || !runState) { + throw new Error("Required animator states not found: idle/walk/run"); + } let idleToWalkTime = 0; let walkToRunTime = 0; let runToWalkTime = 0; @@ -70,7 +73,9 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { idleState.addTransition(toWalkTransition); idleToWalkTime = //@ts-ignore - toWalkTransition.exitTime * idleState._getDuration() + toWalkTransition.duration * walkState._getDuration(); + toWalkTransition.exitTime * idleState._getDuration() + + //@ts-ignore + toWalkTransition.duration * walkState._getDuration(); const exitTransition = idleState.addExitTransition(); exitTransition.addCondition("playerSpeed", AnimatorConditionMode.Equals, 0); @@ -97,16 +102,16 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { toIdleTransition.duration * idleState._getDuration(); // to run state - const RunToWalkTransition = new AnimatorStateTransition(); - RunToWalkTransition.destinationState = walkState; - RunToWalkTransition.duration = 0.3; - RunToWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Less, 0.5); - runState.addTransition(RunToWalkTransition); + const runToWalkTransition = new AnimatorStateTransition(); + runToWalkTransition.destinationState = walkState; + runToWalkTransition.duration = 0.3; + runToWalkTransition.addCondition("playerSpeed", AnimatorConditionMode.Less, 0.5); + runState.addTransition(runToWalkTransition); runToWalkTime = //@ts-ignore - (RunToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() + + (runToWalkTransition.exitTime - toRunTransition.duration) * runState._getDuration() + //@ts-ignore - RunToWalkTransition.duration * walkState._getDuration(); + runToWalkTransition.duration * walkState._getDuration(); stateMachine.addEntryStateTransition(idleState); diff --git a/e2e/case/animator-stateMachineScript.ts b/e2e/case/animator-stateMachineScript.ts index 76e5247b3a..5f6d8d9f26 100644 --- a/e2e/case/animator-stateMachineScript.ts +++ b/e2e/case/animator-stateMachineScript.ts @@ -55,9 +55,12 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { rootEntity.addChild(defaultSceneRoot); const animator = defaultSceneRoot.getComponent(Animator); - const state = animator.findAnimatorState("walk"); + const walkState = animator.animatorController.layers[0].stateMachine.findStateByName("walk"); + if (!walkState) { + throw new Error("Animator state not found: walk"); + } - state.addStateMachineScript( + walkState.addStateMachineScript( class extends StateMachineScript { onStateEnter(animator: Animator, animatorState: AnimatorState, layerIndex: number): void { textRenderer.text = "0"; diff --git a/e2e/case/gltf-blendshape.ts b/e2e/case/gltf-blendshape.ts index 85f3c72b72..5d4c1bde89 100644 --- a/e2e/case/gltf-blendshape.ts +++ b/e2e/case/gltf-blendshape.ts @@ -45,13 +45,14 @@ WebGLEngine.create({ canvas: "canvas" }).then((engine) => { rootEntity.addChild(defaultSceneRoot); const animator = defaultSceneRoot.getComponent(Animator)!; - animator.play("Right"); - const state = animator.getCurrentAnimatorState(0); + // clipStartTime/clipEndTime are on the shared AnimatorState asset, not the per-Animator instance view. + const state = animator.animatorController.layers[0].stateMachine.findStateByName("Right"); state.clipStartTime = 1; state.clipEndTime = 1; + animator.play("Right"); updateForE2E(engine); initScreenshot(engine, camera); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 9470d05817..063fbe1c1c 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -105,6 +105,8 @@ export class Entity extends EngineObject { /** @internal */ _scripts: DisorderedArray