Skip to content

Comments

Allow anyState transitions to interrupt crossFade & fix transition bugs#2885

Merged
cptbtptpbcptdtptp merged 3 commits intogalacean:dev/2.0from
luzhuang:fix/animatorAnyState
Jan 27, 2026
Merged

Allow anyState transitions to interrupt crossFade & fix transition bugs#2885
cptbtptpbcptdtptp merged 3 commits intogalacean:dev/2.0from
luzhuang:fix/animatorAnyState

Conversation

@luzhuang
Copy link
Contributor

@luzhuang luzhuang commented Jan 27, 2026

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

Bug fix

What is the current behavior? (You can also link to an open issue here)

  1. anyState noExitTime transitions 无法打断正在进行的 crossFade 动画
  2. _addTransition 排序时会把 hasExitTime transition 错误地插入到 noExitTime transitions 前面
  3. _checkNoExitTimeTransition 会错误地检查 hasExitTime transitions,可能导致它们在 exitTime 之前触发

What is the new behavior (if this is a feature change)?

  1. 允许 anyState noExitTime transitions 打断 crossFade
  2. 修复 transition 排序和检查逻辑

Changes

CrossFade 打断

_updateCrossFadeState_updateCrossFadeFromPoseState 开头添加 anyState noExitTime transition 检查:

  • 新增 _checkCrossFadeInterrupt 方法,跳过目标状态与当前 crossFade 目标相同的 transition(避免无限循环)
  • 被打断后调用 _updateState 让新状态消费当前帧的 deltaTime

Bug 修复

_addTransition 排序边界错误:排序时只在 [noExitTimeCount, count) 范围内比较,避免插入到 noExitTime transitions 前面

_checkNoExitTimeTransition 循环范围错误:改用 noExitTimeCount 作为循环上限

Does this PR introduce a breaking change? (What changes might users need to make in their application due to this PR?)

No

Other information:

Files Changed:

  • Animator.ts: 添加 crossFade interrupt 逻辑和 _checkCrossFadeInterrupt 方法,修复 _checkNoExitTimeTransition
  • AnimatorStateTransitionCollection.ts: 修复 _addTransition 排序边界
  • Animator.test.ts: 增强 afterEach 清理,新增测试用例

Test: HEADLESS=true npx vitest run tests/src/core/Animator.test.ts - 29 tests passed

Summary by CodeRabbit

  • New Features

    • Cross-fade animations can be immediately interrupted by anyState no-exit-time transitions for more responsive state switching.
  • Improvements

    • No-exit-time transitions are prioritized and ordered separately from exit-time transitions to ensure correct transition scanning and timing.
  • Tests

    • Expanded and hardened tests with improved setup/teardown to ensure deterministic animation transition behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

- Fix _addTransition to respect noExitTimeCount boundary when sorting
  hasExitTime transitions (prevents inserting before noExitTime ones)
- Fix _checkNoExitTimeTransition to use noExitTimeCount instead of count
- Enhance test afterEach to properly clean up transitions and state props
- Add test for noExitTime transition scan ignoring exitTime transitions
@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

Walkthrough

Introduces pre-update interrupt checks that allow AnyState noExitTime transitions to interrupt ongoing cross-fades via a new _checkCrossFadeInterrupt helper and updates transition ordering to keep noExitTime transitions separate from hasExitTime transitions.

Changes

Cohort / File(s) Summary
Cross-Fade Interrupt Logic
packages/core/src/animation/Animator.ts
Added _checkCrossFadeInterrupt and integrated pre-update interrupt checks into cross-fade update paths (including CrossFade from Pose) so AnyState noExitTime transitions can immediately interrupt and consume deltaTime.
Transition Collection Ordering
packages/core/src/animation/AnimatorStateTransitionCollection.ts
Updated _addTransition to keep noExitTime transitions at the front, bound binary-insert comparisons to the noExitTimeCount region, and compute maxExitTime using only hasExitTime transitions.
Test Coverage & Cleanup
tests/src/core/Animator.test.ts
Added comprehensive afterEach cleanup of AnyState/Entry transitions and state attributes, converted mutation tests to try/finally restoration, and added tests for anyState interrupting crossFade and for noExitTime scanning behavior.
Build Configuration
tests/vitest.config.ts
Added "fsevents" to optimizeDeps.exclude.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped between fades, a keen little sprite,
Sniffed an anyState whisper in the night,
"Interrupt!" I cried—so cross-fades gave way,
DeltaTime munched, and states danced all day,
A joyful twitch of ears—hooray! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main changes: allowing anyState transitions to interrupt crossFade and fixing transition bugs in the animator.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@luzhuang luzhuang changed the title Fix/animator any state fix(animation): allow anyState transitions to interrupt crossFade & fix transition bugs Jan 27, 2026
@codecov
Copy link

codecov bot commented Jan 27, 2026

Codecov Report

❌ Patch coverage is 90.74074% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.03%. Comparing base (3414e69) to head (65a205e).
⚠️ Report is 2 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
packages/core/src/animation/Animator.ts 93.61% 3 Missing ⚠️
...src/animation/AnimatorStateTransitionCollection.ts 71.42% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           dev/2.0    #2885   +/-   ##
========================================
  Coverage    83.03%   83.03%           
========================================
  Files          796      796           
  Lines        90383    90434   +51     
  Branches      9488     9503   +15     
========================================
+ Hits         75046    75094   +48     
- Misses       15255    15258    +3     
  Partials        82       82           
Flag Coverage Δ
unittests 83.03% <90.74%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/core/src/animation/AnimatorStateTransitionCollection.ts`:
- Around line 97-116: When a transition flips from noExitTime to hasExitTime you
must decrement the no-exit boundary counter so the noExitTime segment stays
accurate; update the logic in AnimatorStateTransitionCollection (specifically in
the code path that handles inserting a hasExitTime transition—and in
updateTransitionsIndex if it removes a transition from the no-exit segment) to
decrement this.noExitTimeCount when a transition that previously had noExitTime
is moved out of the [0, noExitTimeCount) range, ensuring subsequent scans don't
treat exit-time transitions as no-exit ones.

In `@tests/vitest.config.ts`:
- Around line 13-14: The exclude list currently contains "@galacean/engine-core"
(and related engine packages) and "fsevents"; keep "fsevents" excluded but
remove the "@galacean/engine-core", "@galacean/engine-loader",
"@galacean/engine-rhi-webgl", and "@galacean/engine-math" entries from the
Vitest/Vite optimizeDeps.exclude array unless you have documented pre-bundling
failures for those specific packages—if you do have failures, add a short code
comment referencing the issue/stack trace and keep only the problematic package
names; otherwise delete those "@galacean/..." entries so Vite can pre-bundle
them normally.

Comment on lines +97 to 116
// NoExitTime transitions are stored at the front of the array [0, noExitTimeCount)
if (!transition.hasExitTime) {
transitions.unshift(transition);
this.noExitTimeCount++;
return;
}

// HasExitTime transitions are sorted by exitTime in range [noExitTimeCount, count)
const { exitTime } = transition;
const { noExitTimeCount } = this;
const count = transitions.length;
const maxExitTime = count ? transitions[count - 1].exitTime : 0;
// Only compare with hasExitTime transitions (after noExitTimeCount)
const maxExitTime = count > noExitTimeCount ? transitions[count - 1].exitTime : 0;
if (exitTime >= maxExitTime) {
transitions.push(transition);
} else {
let index = count;
while (--index >= 0 && exitTime < transitions[index].exitTime);
// Stop at noExitTimeCount boundary to avoid comparing with noExitTime transitions
while (--index >= noExitTimeCount && exitTime < transitions[index].exitTime);
transitions.splice(index + 1, 0, transition);
Copy link

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Adjust noExitTimeCount when transitions flip to hasExitTime.

With the new boundary logic, noExitTimeCount must stay accurate. When a transition changes from no-exit-time to exit-time, updateTransitionsIndex(...) removes it from the no-exit segment but doesn’t decrement noExitTimeCount, so exit-time transitions can be scanned as no-exit-time and fire early.

🛠️ Proposed fix
 updateTransitionsIndex(transition: AnimatorStateTransition, hasExitTime: boolean): void {
   const transitions = this.transitions;
-  transitions.splice(transitions.indexOf(transition), 1);
+  const index = transitions.indexOf(transition);
+  if (index !== -1) {
+    transitions.splice(index, 1);
+    if (hasExitTime && index < this.noExitTimeCount) {
+      this.noExitTimeCount--;
+    }
+  }
   this._addTransition(transition);
 }
🤖 Prompt for AI Agents
In `@packages/core/src/animation/AnimatorStateTransitionCollection.ts` around
lines 97 - 116, When a transition flips from noExitTime to hasExitTime you must
decrement the no-exit boundary counter so the noExitTime segment stays accurate;
update the logic in AnimatorStateTransitionCollection (specifically in the code
path that handles inserting a hasExitTime transition—and in
updateTransitionsIndex if it removes a transition from the no-exit segment) to
decrement this.noExitTimeCount when a transition that previously had noExitTime
is moved out of the [0, noExitTimeCount) range, ensuring subsequent scans don't
treat exit-time transitions as no-exit ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method is only called when hasExitTime is modified at runtime. In our current design, transitions are configured during setup, not dynamically toggled during gameplay. Runtime modification of hasExitTime is not a supported use case.

If we need to support dynamic changes in the future, we can revisit this logic then.

Copy link

Choose a reason for hiding this comment

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

@luzhuang, thank you for the clarification! That makes perfect sense—if hasExitTime is only set during the configuration phase and not modified at runtime, then the boundary counter will remain consistent and my concern doesn't apply.

I've noted this design constraint for future reference.

૮ ˶ᵔ ᵕ ᵔ˶ ა
.。.:*♡ noted!


✏️ Learnings added
Learnt from: luzhuang
Repo: galacean/engine PR: 2885
File: packages/core/src/animation/AnimatorStateTransitionCollection.ts:97-116
Timestamp: 2026-01-27T06:23:20.586Z
Learning: In the Galacean engine animator (AnimatorStateTransitionCollection), transitions are configured during setup and not dynamically toggled during gameplay. Runtime modification of `hasExitTime` is not a supported use case.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +13 to +14
"@galacean/engine-core",
"fsevents"
Copy link

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "vitest.config.ts" -o -name "vite.config.ts" | head -20

Repository: galacean/engine

Length of output: 83


🏁 Script executed:

cat -n tests/vitest.config.ts

Repository: galacean/engine

Length of output: 1015


🏁 Script executed:

cat -n package.json | head -50

Repository: galacean/engine

Length of output: 2563


🌐 Web query:

Vite optimizeDeps exclude fsevents Vitest browser testing 2025 2026

💡 Result:

If you’re doing Vitest browser testing and you see Vite’s dependency optimizer trying to pre-bundle fsevents (often via chokidar), the usual fix is to exclude it from Vite/Vitest dep optimization, because fsevents is a macOS-only native Node module (not browser-compatible) [4].

Recommended config (works for Vitest Browser Mode)

Vitest browser mode runs tests through the Vite dev server [3], and Vitest’s optimizer inherits/extends Vite optimizeDeps (web uses optimizeDeps, SSR uses ssr.optimizeDeps) [2]. So excluding it at the Vite level is typically enough:

// vite.config.ts or vitest.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    exclude: ['fsevents'],
  },
})

optimizeDeps.exclude is the official way to exclude deps from pre-bundling [1].

If you need to target Vitest’s optimizer more specifically

You can also exclude via Vitest’s deps optimizer (useful if you don’t want to affect normal Vite dev/build), since it extends Vite optimizeDeps for the relevant mode [2]:

// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    deps: {
      optimizer: {
        web: {
          exclude: ['fsevents'],
        },
      },
    },
  },
})

Sources: Vite dep optimization config (optimizeDeps.exclude) [1]; Vitest deps optimizer inheritance/behavior [2]; Vitest browser mode uses Vite dev server [3]; fsevents is macOS-only [4].


🌐 Web query:

galacean engine-core Vite optimizeDeps exclude recommended

💡 Result:

I couldn’t find any Galacean-specific guidance that recommends adding @galacean/engine-core to vite.optimizeDeps.exclude.

In Vite, optimizeDeps.exclude is only meant for dependencies you must not pre-bundle (typically because pre-bundling breaks them). Vite also warns that CommonJS deps should not be excluded; if you exclude an ESM dep but it pulls in nested CJS, you usually need to add the nested CJS dep to optimizeDeps.include instead. [1]

So the practical recommendation is:

  • Don’t exclude @galacean/engine-core by default.
  • Only add it to optimizeDeps.exclude if you have a reproducible dev-time prebundle error caused by it (and then prefer fixing via optimizeDeps.include / needsInterop as appropriate per Vite’s dep-optimization guidance). [1]

Sources
[1] Vite dep optimization options (optimizeDeps.exclude / CJS warning): https://main.vitejs.dev/config/dep-optimization-options (main.vitejs.dev)

Citations:


Verify if @galacean/engine-* exclusions are necessary; fsevents exclusion is correct.

The fsevents exclusion is appropriate for Vitest browser testing—it's a macOS-only native module incompatible with pre-bundling. However, Vite best practices recommend excluding packages only when pre-bundling causes actual errors. The @galacean/engine-core, @galacean/engine-loader, @galacean/engine-rhi-webgl, and @galacean/engine-math exclusions should be kept only if there are documented pre-bundle failures; otherwise, they should be removed to avoid unnecessary optimization bypass.

🤖 Prompt for AI Agents
In `@tests/vitest.config.ts` around lines 13 - 14, The exclude list currently
contains "@galacean/engine-core" (and related engine packages) and "fsevents";
keep "fsevents" excluded but remove the "@galacean/engine-core",
"@galacean/engine-loader", "@galacean/engine-rhi-webgl", and
"@galacean/engine-math" entries from the Vitest/Vite optimizeDeps.exclude array
unless you have documented pre-bundling failures for those specific packages—if
you do have failures, add a short code comment referencing the issue/stack trace
and keep only the problematic package names; otherwise delete those
"@galacean/..." entries so Vite can pre-bundle them normally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The @galacean/engine-* entries existed before this PR. The only change here was adding fsevents to fix macOS-specific pre-bundling issues. The existing exclusions can be reviewed in a separate PR.

Copy link

Choose a reason for hiding this comment

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

@luzhuang Thank you for the clarification! You're absolutely right—the fsevents addition is the only change in this PR and is the correct fix for macOS-specific pre-bundling issues. The pre-existing @galacean/engine-* exclusions can indeed be evaluated separately if needed. I appreciate you keeping this PR focused on the animator transition fixes. 🐰

@augmentcode
Copy link

augmentcode bot commented Jan 27, 2026

🤖 Augment PR Summary

Summary: This PR fixes Animator “Any State” behavior during cross-fades by allowing eligible AnyState transitions to interrupt an in-progress crossFade.

Changes:

  • Added an early AnyState no-exit-time transition check during cross-fade updates, so crossFades can be interrupted before state updates/events are advanced.
  • Introduced _checkCrossFadeInterrupt to apply AnyState no-exit-time transitions during cross-fade, while avoiding re-targeting the same destination (loop prevention).
  • Adjusted no-exit-time transition scanning to iterate only the noExitTimeCount prefix, preventing exitTime transitions from being evaluated in the no-exit-time pass.
  • Refined AnimatorStateTransitionCollection insertion logic so has-exit-time transitions are ordered only within the exitTime segment (after the no-exit-time prefix).
  • Enhanced test isolation by resetting state machine transitions/state properties between tests.
  • Added tests covering: AnyState interruption of crossFade, and ensuring no-exit-time scans don’t accidentally fire exitTime transitions early.
  • Updated Vitest dependency optimization excludes to include fsevents.

Technical Notes: The implementation relies on the invariant that transitions are stored as [0, noExitTimeCount) for no-exit-time transitions and [noExitTimeCount, count) for exitTime-sorted transitions.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 1 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.


// HasExitTime transitions are sorted by exitTime in range [noExitTimeCount, count)
const { exitTime } = transition;
const { noExitTimeCount } = this;
Copy link

Choose a reason for hiding this comment

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

The new invariant that no-exit-time transitions occupy [0, noExitTimeCount) depends on noExitTimeCount staying correct, but AnimatorStateTransition.hasExitTime calls updateTransitionsIndex() which currently doesn’t adjust noExitTimeCount when a transition flips between exit/no-exit. That can desync the partitioning/sorting and cause no-exit scans (and exit-time indexing) to behave incorrectly after toggling hasExitTime.

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Runtime modification of hasExitTime is not a supported use case. Transitions are configured during setup. If needed in the future, we can revisit this.

…checks

- Add _checkNoExitTimeTransitions as common helper with optional excludeDestState
- _checkNoExitTimeTransition and _checkCrossFadeInterrupt become thin wrappers
@luzhuang
Copy link
Contributor Author

Thanks for the review!

Regarding the updateTransitionsIndex / noExitTimeCount sync issue:

This method is only called when AnimatorStateTransition.hasExitTime is modified at runtime. In our current design, transitions are typically configured during setup (in the editor or at initialization), not dynamically toggled during gameplay. The runtime modification of hasExitTime is not a supported use case.

If we need to support dynamic hasExitTime changes in the future, we can revisit this logic then.

Regarding vitest.config.ts exclusions: The @galacean/engine-* entries existed before this PR. The only change here was adding fsevents to fix macOS-specific pre-bundling issues. The existing exclusions can be reviewed in a separate PR.

@cptbtptpbcptdtptp cptbtptpbcptdtptp merged commit 5e5c616 into galacean:dev/2.0 Jan 27, 2026
12 checks passed
@cptbtptpbcptdtptp cptbtptpbcptdtptp added Animation Built-in animation system related functions bug Something isn't working labels Jan 27, 2026
@cptbtptpbcptdtptp cptbtptpbcptdtptp added this to the 2.0 milestone Jan 27, 2026
@cptbtptpbcptdtptp cptbtptpbcptdtptp changed the title fix(animation): allow anyState transitions to interrupt crossFade & fix transition bugs Allow anyState transitions to interrupt crossFade & fix transition bugs Jan 27, 2026
GuoLei1990 added a commit that referenced this pull request Feb 22, 2026
…ix transition bugs

Re-apply PR #2885 changes on top of reverted dev/2.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Animation Built-in animation system related functions bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants