Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
b5a0ca6
fix(animation): normalize single-root clip binding paths
luzhuang Mar 29, 2026
df4dab8
fix(animation): add per-instance speed to AnimatorStatePlayData
luzhuang Mar 30, 2026
28f5e21
fix(loader): normalize gltf wrapper and skin roots
luzhuang Apr 15, 2026
234164e
refactor(animation): per-state PlayData handle with override-aware speed
luzhuang May 9, 2026
1cc59e8
refactor(animation): clipless-state safety + move PlayData out of int…
luzhuang May 9, 2026
296e5b9
test(animation): per-state PlayData handle and per-instance speed
luzhuang May 9, 2026
fa4ce1b
test(animation): refine per-instance speed regression tests
luzhuang May 9, 2026
8825d6b
fix(loader): _findSceneRootBone walk-up termination
luzhuang May 9, 2026
f58640a
fix(entity): findByPath prefers same-name child over self-prefix
luzhuang May 9, 2026
f2cbd10
fix(e2e): update findAnimatorState callers for new return type
luzhuang May 9, 2026
f297fe8
fix(animation): forbid crossFade to active state and reset orientatio…
luzhuang May 9, 2026
cb5cfd5
fix(entity): tighten findByPath self-prefix fallback
luzhuang May 9, 2026
d53b722
fix(animation): prettier formatting for crossFade alias guard
luzhuang May 9, 2026
bcf9611
docs(animation): update findAnimatorState examples for new API
luzhuang May 9, 2026
722cc2b
fix(e2e): null-guard findAnimatorState callers
luzhuang May 9, 2026
a2d3c2e
fix(loader): _findSceneRootBone only returns wrapper when joints span…
luzhuang May 9, 2026
7fab893
fix(animation): _preparePlay clears stale crossFade slot
luzhuang May 9, 2026
33a676b
fix(animation): crossFade early-return on missing state
luzhuang May 9, 2026
fd1da52
docs(animation): mark playState as @internal
luzhuang May 9, 2026
862ddda
fix(entity): findByPath fallback no longer crashes on null parent
luzhuang May 10, 2026
7462264
fix(animation): _getAnimatorStateInfo bounds-check layerIndex
luzhuang May 10, 2026
4edcd90
fix(animation): guard remaining-time math against zero playSpeed
luzhuang May 10, 2026
affa3ee
docs(loader): document Scene-before-Skin parse-order dependency
luzhuang May 10, 2026
b1e67f2
style(e2e): camelCase local transition variables in animator-stateMac…
luzhuang May 10, 2026
4ecfa0f
test(animation): add missing @ts-ignore on _animatorLayersData[99] ac…
luzhuang May 10, 2026
aa5132c
refactor(animation): align statePlayDataMap with Record + Object.crea…
luzhuang May 10, 2026
34efeaa
docs(animation): clarify pause vs resume in per-instance speed example
luzhuang May 10, 2026
55a96bf
docs(animation): mark self-crossFade no-op as intentional policy
luzhuang May 10, 2026
86a0bf1
perf(loader): _findSceneRootBone avoids Set allocations
luzhuang May 10, 2026
b6205bd
refactor(entity): _findChildByPathDown helper for self-prefix fallback
luzhuang May 10, 2026
e230a51
test(animation): state-machine self-transition is no-op
luzhuang May 10, 2026
36e2f40
test(animation): cover _updatePlayingState playSpeed===0 guard
luzhuang May 10, 2026
4ed7145
refactor(animation): underscore-prefix internal AnimatorStatePlayData…
luzhuang May 10, 2026
500657e
refactor(entity): findByPath polish from review feedback
luzhuang May 10, 2026
e396243
docs(animation): tighten self-cross-fade no-op comment per review
luzhuang May 10, 2026
cc481e1
fix(entity): _findChildByPathDown retries same-name siblings on deep …
luzhuang May 10, 2026
85b82d8
fix(animation): rebuild PlayData when AnimatorState identity changes
luzhuang May 10, 2026
6893ac2
fix(animation): findAnimatorState honors controller update flag
luzhuang May 10, 2026
8c5c9a3
fix(animation): rebuild AnimatorStateData when state identity changes
luzhuang May 10, 2026
4caa2bf
fix(animation): detach stateData listeners on _reset to prevent accum…
luzhuang May 10, 2026
ce40c43
fix(animation): _onDestroy reuses _reset to detach stateData listeners
luzhuang May 10, 2026
15d88f5
revert(entity): remove self-prefix fallback from findByPath
luzhuang May 11, 2026
9d296b0
refactor(animation): collapse getOrCreatePlayData guard with optional…
luzhuang May 11, 2026
5859036
refactor(animation): drop clearSpeedOverride, treat speed write as final
luzhuang May 11, 2026
eec99ad
refactor(animation): tighten AnimatorStateData lifecycle invariants
luzhuang May 11, 2026
aed9e10
docs(animation): document getCurrentAnimatorState nullable return
luzhuang May 11, 2026
88ff930
docs(loader): warn against Skin-await-Scene cycle next to Scene-befor…
luzhuang May 11, 2026
6230dca
refactor(loader): drop _findSceneRootBone, resolve skin rootBone via …
luzhuang May 11, 2026
31e711e
docs(loader): update Scene-before-Skin comment after LCA unification
luzhuang May 11, 2026
82026ee
refactor(animation): rename _speedOverride to _speed after dropping o…
luzhuang May 11, 2026
35a3147
refactor(animation): extract _resetIfControllerUpdated helper
luzhuang May 11, 2026
479a8be
docs(animation): tighten AnimatorStatePlayData class doc and explain …
luzhuang May 11, 2026
4457f62
docs(loader): explain why _sceneRoots[i] is written synchronously
luzhuang May 11, 2026
31344e5
docs(loader): tighten skin rootBone resolution comments
GuoLei1990 May 14, 2026
7882a1f
revert(loader): drop orphan _sceneRoots[i] synchronous write
GuoLei1990 May 14, 2026
b109a42
docs(loader): tighten skin rootBone resolution comments
GuoLei1990 May 14, 2026
ddcb9ee
refactor(loader): revert _findSkeletonRootBone rename
GuoLei1990 May 14, 2026
efa4795
docs(entity): fix stale @returns text on getChild and findByName
GuoLei1990 May 14, 2026
fe78f5e
refactor(animation): split AnimatorState into asset (Def) and per-ins…
GuoLei1990 May 15, 2026
e94f036
refactor(animation): adapt tests, e2e and docs to AnimatorState view API
GuoLei1990 May 15, 2026
47fc4ba
refactor(animation): demote AnimatorState.def to @internal
GuoLei1990 May 15, 2026
4c84eba
Merge branch 'dev/2.0' into redesign-animator-state-view
GuoLei1990 May 15, 2026
a4a6974
refactor(animation): rename per-instance view to AnimatorStateInstance
GuoLei1990 May 15, 2026
f1709c4
refactor(animation): trim verbose comments on instance/runtime types …
GuoLei1990 May 15, 2026
12d3ee4
refactor(animation): rename Def-suffixed locals in e2e cases to State
GuoLei1990 May 15, 2026
6c26f4b
refactor(animation): drop implementation details from public JSDoc
GuoLei1990 May 15, 2026
92529bb
feat(animation): make wrapMode per-instance overrideable on AnimatorS…
GuoLei1990 May 15, 2026
0e30020
refactor(animation): restore AnimatorStatePlayData naming
GuoLei1990 May 15, 2026
50d9ff9
refactor(animation): clean up Instance/PlayData ownership and naming
GuoLei1990 May 15, 2026
a0c21a3
refactor(animation): drop redundant layerIndex<0 check after !state g…
GuoLei1990 May 15, 2026
86b8398
fix(animation): dispatch controller update flag on stateMachine state…
GuoLei1990 May 15, 2026
8292d1a
refactor(animation): lazy version + WeakMap for state-keyed caches
GuoLei1990 May 15, 2026
4b4fe7f
refactor(animation): drop unused event handler pool, rename to _ensur…
GuoLei1990 May 15, 2026
efc428f
style(animation): drop redundant self-rename in destructures
GuoLei1990 May 15, 2026
4e7d18d
refactor(animation): collapse cross-fade slot reset into AnimatorLaye…
GuoLei1990 May 15, 2026
04cb804
docs(animation): expand AnimatorStateInstance JSDoc
GuoLei1990 May 15, 2026
ca72f7f
style(animation): expand single-line getter JSDoc to multi-line
GuoLei1990 May 15, 2026
faf95c6
fix(animation): clear defaultState on removeState to drop dangling ref
GuoLei1990 May 15, 2026
fb529ba
style(updateflagmanager): drop redundant prose comment on _version
GuoLei1990 May 15, 2026
e6b861f
style(animatorstatedata): drop redundant prose comment on eventsBuilt…
GuoLei1990 May 15, 2026
04009e0
refactor(animation): revert AnimatorStatePlayData.resetForPlay to reset
GuoLei1990 May 15, 2026
8e1c131
style(animatorlayerdata): drop redundant method JSDoc
GuoLei1990 May 15, 2026
f167d01
fix(animation, loader): test assertion + getCurrentAnimatorState rese…
GuoLei1990 May 15, 2026
7b52bfc
fix(animation): ensure event handlers in update path; fix stale test …
GuoLei1990 May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions docs/en/animation/animator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
39 changes: 28 additions & 11 deletions docs/zh/animation/animator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
```

### 过渡指定动画状态
Expand All @@ -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;
```

### 动画裁剪
Expand Down
6 changes: 5 additions & 1 deletion e2e/case/animator-additive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions e2e/case/animator-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion e2e/case/animator-play-backwards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
27 changes: 16 additions & 11 deletions e2e/case/animator-stateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);

Expand Down
7 changes: 5 additions & 2 deletions e2e/case/animator-stateMachineScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Comment on lines +58 to +61
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard animator and layer before walk-state lookup.

animator and layers[0] can fail before your explicit "walk" error path, giving a less actionable crash. Add explicit checks first.

Suggested patch
-      const animator = defaultSceneRoot.getComponent(Animator);
-      const walkState = animator.animatorController.layers[0].stateMachine.findStateByName("walk");
+      const animator = defaultSceneRoot.getComponent(Animator);
+      if (!animator) {
+        throw new Error("Animator component not found on scene root");
+      }
+      const layer = animator.animatorController.layers[0];
+      if (!layer) {
+        throw new Error("Animator layer not found: index 0");
+      }
+      const walkState = layer.stateMachine.findStateByName("walk");
       if (!walkState) {
         throw new Error("Animator state not found: walk");
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/case/animator-stateMachineScript.ts` around lines 58 - 61, The code
directly accesses animator.animatorController.layers[0] before verifying the
parent objects exist, so guard against null/undefined to produce clearer errors:
add checks for animator, animator.animatorController, and
animator.animatorController.layers.length > 0 (or layers[0] truthiness) before
calling findStateByName; if any check fails, throw distinct errors like
"Animator missing", "AnimatorController missing", or "Animator has no layers" so
the failure is actionable, then proceed to call
animator.animatorController.layers[0].findStateByName("walk") and keep the
existing walk-state not found error.


state.addStateMachineScript(
walkState.addStateMachineScript(
class extends StateMachineScript {
onStateEnter(animator: Animator, animatorState: AnimatorState, layerIndex: number): void {
textRenderer.text = "0";
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,16 +341,16 @@ export class Entity extends EngineObject {
* @deprecated Please use `children` property instead.
* Find child entity by index.
* @param index - The index of the child entity
* @returns The component which be found
* @returns The entity that was found
*/
getChild(index: number): Entity {
return this._children[index];
}

/**
* Find entity by name.
* @param name - The name of the entity which want to be found
* @returns The component which be found
* @param name - The name of the entity to find
* @returns The entity that was found
*/
findByName(name: string): Entity {
if (name === this.name) {
Expand All @@ -369,14 +369,13 @@ export class Entity extends EngineObject {
/**
* Find the entity by path.
* @param path - The path of the entity eg: /entity
* @returns The component which be found
* @returns The entity that was found
*/
findByPath(path: string): Entity {
const splits = path.split("/").filter(Boolean);
if (!splits.length) {
return this;
}

return Entity._findChildByName(this, 0, splits, 0);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/UpdateFlagManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Utils } from "./Utils";
export class UpdateFlagManager {
/** @internal */
_updateFlags: UpdateFlag[] = [];
/** @internal */
_version: number = 0;

private _listeners: ((type?: number, param?: Object) => void)[] = [];

Expand Down Expand Up @@ -62,6 +64,8 @@ export class UpdateFlagManager {
* @param param - Event param
*/
dispatch(type?: number, param?: Object): void {
this._version++;

const updateFlags = this._updateFlags;
for (let i = updateFlags.length - 1; i >= 0; i--) {
updateFlags[i].dispatch(type, param);
Expand Down
Loading
Loading