Skip to content

fix(animation, physics, audio, gltf): cherry-pick fixes from fix/shaderlab#3014

Open
luzhuang wants to merge 6 commits into
dev/2.0from
fix/loader-animation-physics
Open

fix(animation, physics, audio, gltf): cherry-pick fixes from fix/shaderlab#3014
luzhuang wants to merge 6 commits into
dev/2.0from
fix/loader-animation-physics

Conversation

@luzhuang
Copy link
Copy Markdown
Contributor

@luzhuang luzhuang commented May 25, 2026

Summary

fix/shaderlab 分支抽离 Animation、Physics、Audio、GLTF Loader 相关修复,合入 dev/2.0

Animation

  • 重构 Animator 状态管理:用 AnimatorStatePlayData 承载 per-instance 的 speed/wrapMode,替代 AnimatorStateInstance 代理模式
  • AnimationClip.sampleAnimation() 公开 API,支持外部采样动画
  • Animator.fireEvents 开关,控制是否触发 AnimationEvent 回调
  • _saveAnimatorEventHandlers 用 listener 模式监听 clip 变更,_reset() 时正确移除 listener 防止泄漏
  • _crossFade 增加 null guard,防止 _getAnimatorStateInfo 返回空 state 时崩溃
  • _updatePlayingState 增加 playSpeed === 0 除零保护,防止 NaN
  • 动画事件处理器使用 ClearableObjectPool 池化复用

Physics

  • Collider 增加 _teleportToEntityTransform,re-enable 时 teleport 而非 sweep
  • DynamicCollider 增加 applyForceAtPositionsetKinematicTarget
  • DynamicCollider 移除 addForce/addTorque 中冗余的 wakeUp 调用
  • Collision 碰撞法线方向修正(shape0→shape1 约定)
  • MeshColliderShape 增加 cooking 自动重试
  • PhysXPhysics 支持 tolerance scale 配置
  • PhysicsMaterial 属性同步修复
  • CharacterController 增加 _teleportToEntityTransform override
  • 修复多项物理测试(Collision、DynamicCollider、MeshColliderShape、PhysicsMaterial、PhysicsScene)

Audio

  • AudioManager 重构前后台生命周期管理:hide/show 时自动 suspend/resume playing sources
  • AudioSource pending playback 注册到 AudioManager,由 context 恢复时统一驱动
  • 增加 _suspendPlaybackForInterruption / _resumeInterruptedPlayback 机制
  • _initSourceNode 增加 offset 边界处理和非循环播放结束保护
  • AudioLoader 新增 "audio" 扩展名支持
  • 新增 AudioSourcePendingPlayback 测试覆盖 autoplay blocking、前后台恢复、手势解锁等场景

GLTF Loader

  • GLTFSceneParser 存储 _sceneRoots_computeSkinnedBounds 简化为直接用 rootBone worldMatrix 逆矩阵
  • GLTFSkinParser 重构 _findSkinRootBoneByLCA,skin root 解析时包含 skinned mesh 节点参与 LCA 计算
  • GLTFExtensionSchemaIGalaceanMaterialRemap 改为独立的 url/key 字段(去除 RefItem 依赖)
  • GLTFUtils.loadImageBuffer 恢复 URL.revokeObjectURL 防止 blob URL 泄漏
  • 新增 GLTFSkinParser.test.ts 测试 rootBone LCA 解析

Test plan

  • npx vitest run tests/src/core/Animator.test.ts
  • npx vitest run tests/src/core/physics/
  • npx vitest run tests/src/core/audio/
  • npx vitest run tests/src/loader/GLTFLoader.test.ts
  • 验证 GLTF 动画加载路径(单根/多根场景)
  • 验证物理碰撞 clone、raycast、kinematic teleport
  • 验证音频后台恢复和 autoplay blocking 恢复

Summary by CodeRabbit

Release Notes

  • New Features

    • Added public animation sampling method for custom playback control
    • Improved audio playback recovery with gesture-based resume handling
    • Added force application at specific positions for physics bodies
    • Introduced kinematic transform synchronization mode options
    • Enhanced animation event firing with configurable controls
  • Bug Fixes

    • Fixed contact normal direction calculations in physics collisions
    • Improved mesh collider shape creation with automatic retry mechanism
    • Corrected character controller positioning during teleportation
    • Fixed animation event firing behavior for looped clips
  • Tests

    • Expanded animation playback and audio suspension test coverage
    • Added physics collision and material cloning validation tests

Review Change Stack

Loader:
- componentRef 解析支持 clone entity 子树查找
- always create GLTF_ROOT container for consistent animation paths
- normalize gltf wrapper and skin roots
- include skinned mesh nodes in skin root LCA
- compute skinned bounds from non-joint rootBone
- add audio extension to AudioLoader supported types
- recognize .m4a/.aac/.flac extensions for audioClip loader
- apply scene physics settings
- cover stripped entity child attachment

Animation:
- normalize single-root clip binding paths
- add per-instance speed to AnimatorStatePlayData
- use per-instance speed during cross fade
- update animator
- expose clip sampling and gate animation events

Physics:
- resolve clone-bypass and runtime native handle desync bugs
- add DynamicCollider.applyForceAtPosition
- remove redundant wakeUp in addForce/addTorque
- repair 8 pre-existing test failures (Lite material + PhysX scene queries)
- kinematic transform sync via setKinematicTarget
- correct collision contact normal orientation
- support kinematic transform teleport sync
- teleport collider on re-enable instead of sweeping
- support PhysX tolerance scale
- raycast 和 sweep 跳过 initial overlap

Clone:
- prefab 克隆时自动 deep clone 同类型 Object 属性

Audio:
- restore pending playback correctly
- restore sources after background interruption
- recover audio after foreground resume
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 25, 2026

This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment "augment review" and we will review it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

Walkthrough

This PR refactors the animation system's state-play-data model to use per-instance speed/wrap modes with pooled event handlers, introduces physics collider transform synchronization modes and optional tolerances defaults, implements comprehensive audio suspend/resume with user-gesture and visibility detection, and updates GLTF skin parsing to use lowest-common-ancestor root bone resolution.

Changes

Animation State and Play-Data Refactoring

Layer / File(s) Summary
AnimatorStatePlayData and AnimatorLayerData structure
packages/core/src/animation/internal/AnimatorStatePlayData.ts, packages/core/src/animation/internal/AnimatorLayerData.ts, packages/core/src/animation/internal/AnimatorStateData.ts
AnimatorStatePlayData now stores AnimatorState directly and initializes per-instance speed and wrapMode; AnimatorLayerData switches from WeakMap to string-keyed Record<string, AnimatorStateData> and eagerly initializes non-nullable srcPlayData/destPlayData slots with switchPlayData() to swap them.
Pooled event handlers and lifecycle
packages/core/src/animation/Animator.ts, packages/core/src/animation/internal/AnimationEventHandler.ts
AnimationEventHandler implements IPoolElement for pooling; Animator adds fireEvents boolean and ClearableObjectPool allocator; _saveAnimatorEventHandlers builds pooled handlers once per state and wires a clip-changed listener that rebuilds via the pool on clip updates.
Public API and state resolution
packages/core/src/animation/Animator.ts, packages/core/src/animation/AnimationClip.ts, packages/core/src/animation/index.ts
getCurrentAnimatorState() and findAnimatorState() now return AnimatorState and AnimatorStatePlayData instead of AnimatorStateInstance; AnimationClip exposes public sampleAnimation() wrapper; index re-exports AnimatorStatePlayData instead of AnimatorStateInstance.
Reset and prepare paths
packages/core/src/animation/Animator.ts
_reset() expanded to remove clip-changed listeners, clear per-layer state maps, clear pooled event handlers, and clear controller update flag; _preparePlay and _prepareCrossFadeByTransition refactored to reset per-play-data with AnimatorStateData.
Animation evaluation loops
packages/core/src/animation/Animator.ts
Multiple paths (_updatePlayingState, _updateCrossFadeState, _fireAnimationEvents, etc.) updated to use per-play-data state/clip/speed references; event firing respects WrapMode.Loop wrapping logic; cross-fade no longer applies self/active-destination no-op guard.
Animator tests
tests/src/core/Animator.test.ts
Tests updated to use handle-based state APIs and assert per-play-data speed/wrapMode per-instance overrides; new tests for fireEvents gating and sampleAnimation curve sampling without events; large legacy test block removed.

Physics Collider Synchronization and Defaults

Layer / File(s) Summary
Collider teleport and sync hooks
packages/core/src/physics/Collider.ts
Collider adds _pendingReenterTeleport flag and branches in _onUpdate to teleport on re-enable or sync normally; introduces protected _teleportToEntityTransform (instant) and _syncEntityTransformToNative (physics-aware) methods; per-shape _onPhysicsUpdate() called each frame.
CharacterController and contact defaults
packages/core/src/physics/CharacterController.ts, packages/core/src/physics/shape/ColliderShape.ts, packages/design/src/physics/IPhysics.ts
CharacterController adds protected _teleportToEntityTransform override; ColliderShape._contactOffset made optional with getter fallback to engine default then 0.02; IPhysics interface adds getDefaultContactOffset() and getDefaultSleepThreshold() hooks.
DynamicCollider force-at-position and sync mode
packages/core/src/physics/DynamicCollider.ts
applyForceAtPosition() computes torque from center-of-mass and applies both force and torque; new DynamicColliderKinematicTransformSyncMode enum (Target vs Teleport) controls how kinematic entity transforms sync via _syncEntityTransformToNative override; sleepThreshold getter uses engine default when unset.
PhysicsMaterial sync and MeshCollider retry
packages/core/src/physics/PhysicsMaterial.ts, packages/core/src/physics/shape/MeshColliderShape.ts
PhysicsMaterial adds _cloneTo and _syncNative to refresh native properties; MeshColliderShape introduces _pendingNativeShapeCreation flag and per-tick _onPhysicsUpdate retry until cook succeeds.
Collision normal and PhysX tolerances
packages/core/src/physics/Collision.ts, packages/physics-physx/src/PhysXPhysics.ts, packages/physics-physx/src/PhysXDynamicCollider.ts
Contact normal factor now uses this.shape.id vs shape1Id directly; PhysXPhysics adds PhysXPhysicsOptions for configurable tolerances scale with computed defaults; PhysXDynamicCollider caches kinematic/CCD state and applies mode conditionally on transitions.
Physics tests
tests/src/core/physics/*.test.ts
Tests updated to import WebGLEngine from RHI package; new tests for contact normals, billiard collisions, kinematic callbacks, applyForceAtPosition, kinematic teleport re-enable, CCD toggles, and cloned material behavior; physics-lite switches to no-op setters with one-time warning.

Audio Suspend/Resume with User-Gesture Recovery

Layer / File(s) Summary
AudioManager lifecycle and gesture detection
packages/core/src/audio/AudioManager.ts
Tracks pending/playing/interrupted sources via sets; suspend() interrupts active sources before suspending context; resume() resumes pending/interrupted sources and handles cases where context doesn't exist; binds visibility/pagehide/gesture event listeners once with hidden-state detection and foreground restore timer/delay.
AudioSource pending and interruption handling
packages/core/src/audio/AudioSource.ts
Refactors play/stop/pause to manage _pendingPlay and interruption state; introduces _resumePendingPlayback, _suspendPlaybackForInterruption, _resumeInterruptedPlayback; _initSourceNode returns boolean and fails on invalid offset/duration, resetting timing.
AudioSource pending playback tests
tests/src/core/audio/AudioSourcePendingPlayback.test.ts
New test suite with mocked AudioContext, GainNode, AudioBufferSourceNode covering pending replay after user gesture, cancellation before gesture, no-op resume without context, interruption on visibility change, recovery via gesture/timer/pagehide, and pending retry in later gesture.

GLTF Loader Skin and Schema Updates

Layer / File(s) Summary
GLTFSkinParser LCA root bone resolution
packages/loader/src/gltf/parser/GLTFSkinParser.ts, packages/loader/src/gltf/parser/GLTFSkinParser.test.ts
Replaces _findSkeletonRootBone with _findSkinRootBoneByLCA which expands joint set by scanning nodes with matching skin index, then computes lowest common ancestor; new test suite validates root bone selection correctness.
GLTFSceneParser bounds and extension schema
packages/loader/src/gltf/parser/GLTFSceneParser.ts, packages/loader/src/gltf/extensions/GLTFExtensionSchema.ts
Scene parser caches per-scene roots in glTFResource._sceneRoots; _computeLocalBounds replaces approximate bind-matrix averaging with inverse root-bone world matrix; IGalaceanMaterialRemap refactored to standalone interface without RefItem base.
Loader tests and formatting
tests/src/loader/GLTFLoader.test.ts, tests/src/loader/PrefabResource.test.ts, tests/src/loader/RenderTargetLoader.test.ts
GLTF tests updated to use RHI WebGL engine import with adjusted mocked glTF structures for LCA logic and new bounds validation test; prefab and render target test fixtures reformatted for readability.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

physics, Animation

Suggested reviewers

  • GuoLei1990

🐰 Hops through animation states with pooled handlers so fleet,
Physics colliders sync with defaults, now forces complete,
Audio dances through gestures when hidden or shown,
While GLTF roots find kin in LCA's own zone!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/loader-animation-physics

Copy link
Copy Markdown

@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: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/core/src/physics/shape/MeshColliderShape.ts (1)

199-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear pending retry state for non-retryable mesh collider config.

When non-convex is used on a non-kinematic DynamicCollider, _createNativeShape() returns early but _pendingNativeShapeCreation can remain true (set in set mesh), so _onPhysicsUpdate() keeps retrying and logging every tick.

💡 Proposed fix
   private _createNativeShape(): void {
     // Non-convex MeshColliderShape is only supported on StaticCollider or kinematic DynamicCollider
     if (!this._isConvex && this._collider instanceof DynamicCollider && !this._collider.isKinematic) {
       console.error("MeshColliderShape: Non-convex mesh is not supported on non-kinematic DynamicCollider.");
+      this._pendingNativeShapeCreation = false;
       return;
     }

Also applies to: 248-250

🤖 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 `@packages/core/src/physics/shape/MeshColliderShape.ts` around lines 199 - 203,
The method _createNativeShape() returns early when a non-convex mesh is used on
a non-kinematic DynamicCollider but does not clear the retry flag
_pendingNativeShapeCreation, causing _onPhysicsUpdate() to repeatedly retry;
update _createNativeShape() to set this._pendingNativeShapeCreation = false
before each early return (including the non-convex/non-kinematic DynamicCollider
branch and the other early-return branches around the same area) so pending
retry state is cleared when the mesh config is non-retryable.
packages/loader/src/SceneLoader.ts (1)

1-13: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix missing Material import in SceneLoader sky-material lookup

packages/loader/src/SceneLoader.ts uses Material in resourceManager.getResourceByRef<Material>(background.skyMaterial), but Material is not imported from @galacean/engine-core, which will fail TypeScript compilation.

➕ Suggested fix
 import {
   AssetPromise,
   AssetType,
   BackgroundMode,
   DiffuseMode,
   Loader,
   LoadItem,
   Logger,
+  Material,
   Mesh,
   resourceLoader,
   ResourceManager,
   Scene
 } from "`@galacean/engine-core`";
🤖 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 `@packages/loader/src/SceneLoader.ts` around lines 1 - 13, The import list in
SceneLoader.ts is missing Material, causing the generic call
resourceManager.getResourceByRef<Material>(background.skyMaterial) to fail;
update the import from "`@galacean/engine-core`" in SceneLoader.ts to include
Material alongside AssetPromise, AssetType, BackgroundMode, DiffuseMode, Loader,
LoadItem, Logger, Mesh, resourceLoader, ResourceManager, and Scene so the
Material type is available for the skyMaterial lookup and TypeScript compilation
succeeds.
packages/core/src/animation/Animator.ts (1)

400-415: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing layer-index bounds check can throw runtime errors.

At Line 414, layers[layerIndex] is accessed without validating range. Invalid layerIndex values should safely return no state instead of throwing.

🤖 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 `@packages/core/src/animation/Animator.ts` around lines 400 - 415, In
_getAnimatorStateInfo, avoid indexing layers with an out-of-range layerIndex:
before accessing layers[layerIndex].stateMachine (or calling findStateByName on
a specific layer), validate that layerIndex is >= 0 and < layers.length; if the
index is invalid, return the default _tempAnimatorStateInfo (no state) instead
of proceeding. Ensure both the else branch that uses layers[layerIndex] and any
post-loop access paths check bounds, and keep references to symbols:
_getAnimatorStateInfo, _animatorController, layers, layerIndex,
stateMachine.findStateByName, and _tempAnimatorStateInfo to locate and update
the code.
🟠 Major comments (23)
.husky/_/husky.sh-1-1 (1)

1-1: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace the accidental local absolute path with the real Husky helper.

Line 1 hardcodes a workstation-specific path, so hooks will fail on any other checkout, and this also leaks a local filesystem path into the repo. Please restore/regenerate the actual .husky/_/husky.sh script instead of committing a local absolute path literal.

🤖 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 @.husky/_/husky.sh at line 1, The committed .husky/_/husky.sh contains a
workstation-specific absolute path instead of the real Husky helper script;
remove the hardcoded path and restore the proper husky helper by regenerating or
replacing the file with Husky's shipped script (the standard husky.sh helper
used by Git hooks). To fix, delete the current .husky/_/husky.sh, then
regenerate the helper by running the Husky install command (or copy the husky.sh
helper from the Husky package) so the file contains the canonical husky helper
content rather than an absolute local path; verify hooks invoke the restored
husky.sh helper correctly.
tests/src/core/particle/ParticleStopResume.test.ts-85-89 (1)

85-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tests are asserting known-bug behavior instead of expected behavior.

Line 87 and Line 140 currently validate the bug path, which hardens the defect into CI and blocks the real fix later. Please switch these to expected-correct assertions (or mark as it.fails/it.todo until the fix lands).

Also applies to: 137-140

🤖 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 `@tests/src/core/particle/ParticleStopResume.test.ts` around lines 85 - 89, The
test currently asserts the known-bug behavior instead of the correct behavior;
update the failing assertions that reference emittedDuringIdle,
emittedFirstFrameAfterResume and the time delta playTimeAfterIdle -
playTimeAtStop to reflect the expected-correct outcomes (e.g.,
emittedFirstFrameAfterResume should assert a small/single catch-up like
toBeLessThanOrEqual(1) and playTimeAfterIdle - playTimeAtStop should assert a
small delta like toBeLessThanOrEqual(1) or toBeCloseTo) OR, if you cannot fix
behavior now, mark the affected test cases (the Particle stop/resume test that
uses those variables) as it.todo or it.fails so CI does not lock in the buggy
expectations.
packages/loader/src/gltf/parser/GLTFSkinParser.test.ts-5-7 (1)

5-7: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Global window is being clobbered and not restored.

Line 5 replaces the entire window object, which can pollute global state and cause cross-test flakiness. Stub only required fields and restore after each test.

🔧 Safer isolation pattern
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";

 describe("GLTFSkinParser rootBone resolution", () => {
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });
+
   async function createParser(): Promise<any> {
-    (globalThis as any).window = { AudioContext: undefined, TextMetrics: undefined };
+    vi.stubGlobal("window", {
+      ...(globalThis as any).window,
+      AudioContext: undefined,
+      TextMetrics: undefined
+    });
     const { GLTFSkinParser } = await import("./GLTFSkinParser");
     return new GLTFSkinParser();
   }
🤖 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 `@packages/loader/src/gltf/parser/GLTFSkinParser.test.ts` around lines 5 - 7,
The test currently replaces globalThis.window entirely which can pollute global
state; instead, in GLTFSkinParser.test.ts stub only the needed properties (e.g.,
set globalThis.window = globalThis.window ?? {} then assign window.AudioContext
and window.TextMetrics) or better save const _origWindow = globalThis.window and
replace only the missing properties, and restore the original window in an
afterEach/teardown block; ensure the test still imports GLTFSkinParser after
stubbing and that the restoration happens even if the test errors so other tests
are unaffected.
packages/core/src/animation/AnimatorStateMachine.ts-22-23 (1)

22-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

defaultState can become dangling after removeState

Line 22 makes defaultState non-null, but Line 66-73 does not rebind it when the current default is removed. This can leave auto-play pointing to a state no longer in states/_statesMap.

🔧 Suggested fix
-  defaultState: AnimatorState;
+  defaultState: AnimatorState | null = null;
@@
   removeState(state: AnimatorState): void {
@@
     if (index > -1) {
       this.states.splice(index, 1);
     }
     delete this._statesMap[name];
+    if (this.defaultState === state) {
+      this.defaultState = this.states[0] ?? null;
+    }
   }

Also applies to: 66-73

🤖 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 `@packages/core/src/animation/AnimatorStateMachine.ts` around lines 22 - 23,
The defaultState property can become dangling when removeState removes the
currently assigned default; update the removeState method (and any code that
mutates states/_statesMap or autoplay behavior) to check if the state being
removed === this.defaultState and, if so, rebind this.defaultState to either
null/undefined or to a safe fallback state (e.g., the first remaining state in
states/_statesMap) and update autoplay handling accordingly so auto-play never
references a removed state; also make defaultState nullable in the class
signature if you choose to allow null to represent "no default".
packages/loader/src/resource-deserialize/resources/parser/ParserContext.ts-42-49 (1)

42-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset dependency-progress state in clear().

Lines 42-49 clear entity/component collections but leave _tasks, _loaded, and _total unchanged. If this context is reused, URL dedupe and progress reporting become stale.

🔧 Proposed fix
   clear() {
     this.entityMap.clear();
     this.components.clear();
     this.componentConfigMap.clear();
     this.entityConfigMap.clear();
     this.rootIds.length = 0;
     this.strippedIds.length = 0;
+    this._tasks.clear();
+    this._loaded = 0;
+    this._total = 0;
   }
🤖 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 `@packages/loader/src/resource-deserialize/resources/parser/ParserContext.ts`
around lines 42 - 49, The clear() method on ParserContext currently resets
entity/component collections but fails to reset dependency/progress state
(_tasks, _loaded, _total), causing stale URL dedupe and progress when the
context is reused; update ParserContext.clear() to clear or reinitialize _tasks
(e.g., call _tasks.clear() or assign a new empty map/set) and reset numeric
counters _loaded and _total back to 0 so dependency tracking and progress
reporting start fresh.
packages/loader/src/ShaderLoader.ts-21-24 (1)

21-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when @builtin resolves to an unknown shader in ShaderLoader

Shader.find(name) is return Shader._shaderMap[name], so an unknown name returns a missing entry at runtime. ShaderLoader currently returns Shader.find(builtinShader) directly, while MaterialLoader already guards if (shader) ... before using it—so ShaderLoader should reject/throw instead of resolving with an invalid value.

🔧 Proposed fix
       const builtinShader = this._getBuiltinShader(code);
       if (builtinShader) {
-        return Shader.find(builtinShader);
+        const shader = Shader.find(builtinShader);
+        if (!shader) {
+          throw new Error(`ShaderLoader: builtin shader "${builtinShader}" not found.`);
+        }
+        return shader;
       }
🤖 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 `@packages/loader/src/ShaderLoader.ts` around lines 21 - 24, The current
ShaderLoader returns Shader.find(builtinShader) directly which can yield
undefined for unknown builtins; update the logic in ShaderLoader (use the
_getBuiltinShader method and the call to Shader.find) to check the result and
throw or reject with a clear error if Shader.find(builtinShader) is falsy
(include the builtin name in the message), instead of returning the invalid
value so callers fail fast.
packages/loader/src/MaterialLoader.ts-24-32 (1)

24-32: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard missing renderState before applying it.

When the schema omits renderState, this code currently assigns material.renderState = undefined, which wipes the material’s default render-state object. That turns a missing optional field into a runtime regression for otherwise valid .mat files.

🛠️ Proposed fix
-function parseProperty(object: Object, key: string, value: any) {
-  if (typeof value === "object") {
-    for (let subKey in value) {
-      parseProperty(object[key], subKey, value[subKey]);
-    }
-  } else {
-    object[key] = value;
-  }
+function parseProperty(object: any, key: string, value: any) {
+  if (value == null) {
+    return;
+  }
+
+  if (typeof value === "object") {
+    const target = object[key];
+    if (target == null) {
+      return;
+    }
+
+    for (const subKey in value) {
+      parseProperty(target, subKey, value[subKey]);
+    }
+  } else {
+    object[key] = value;
+  }
 }
@@
-    parseProperty(material, "renderState", renderState);
+    renderState && parseProperty(material, "renderState", renderState);

Also applies to: 125-126

🤖 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 `@packages/loader/src/MaterialLoader.ts` around lines 24 - 32, The
parseProperty function currently assigns nested keys blindly and ends up setting
material.renderState = undefined when the schema omits renderState; update
parseProperty (and the code paths that call it for material.renderState, e.g.,
where material.renderState is populated around the renderState handling at the
second occurrence) to first check that object[key] (i.e., material.renderState)
exists and, if not, create or skip applying subkeys as appropriate—specifically:
when key === "renderState" ensure you do not overwrite an existing
material.renderState with undefined (create a default object only if needed) or
skip assignment when value is undefined so missing optional renderState does not
wipe defaults.
packages/loader/src/resource-deserialize/resources/scene/SceneParser.ts-48-53 (1)

48-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Load sky dependencies independently instead of requiring both refs.

At Line 48, dependency collection only runs when both skyMesh and skyMaterial are present. If only one is configured, it won’t be collected.

💡 Suggested fix
-      const { skyMesh, skyMaterial } = background;
-      if (skyMesh && skyMaterial) {
-        // `@ts-ignore`
-        context._addDependentAsset(skyMesh.url, resourceManager.getResourceByRef(skyMesh));
-        // `@ts-ignore`
-        context._addDependentAsset(skyMaterial.url, resourceManager.getResourceByRef(skyMaterial));
-      }
+      const { skyMesh, skyMaterial } = background;
+      // `@ts-ignore`
+      skyMesh && context._addDependentAsset(skyMesh.url, resourceManager.getResourceByRef(skyMesh));
+      // `@ts-ignore`
+      skyMaterial && context._addDependentAsset(skyMaterial.url, resourceManager.getResourceByRef(skyMaterial));
🤖 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 `@packages/loader/src/resource-deserialize/resources/scene/SceneParser.ts`
around lines 48 - 53, The current logic only adds sky dependencies when both
skyMesh and skyMaterial are truthy; change it to add each dependency
independently: if skyMesh is present call
context._addDependentAsset(skyMesh.url,
resourceManager.getResourceByRef(skyMesh)), and if skyMaterial is present call
context._addDependentAsset(skyMaterial.url,
resourceManager.getResourceByRef(skyMaterial)) so each is collected even when
the other is missing; update the checks around skyMesh and skyMaterial in
SceneParser (the block using context._addDependentAsset and
resourceManager.getResourceByRef) accordingly.
packages/loader/src/Texture2DLoader.ts-49-52 (1)

49-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the original mipmap decision during content restore.

The initial image path only generates mipmaps when supportGenerateMipmapsWithCorrection(...) says it is valid, but restoreContent() always regenerates them. Textures loaded with mipmap: false or unsupported dimensions/format will restore to different GPU state after context loss.

🧩 Suggested fix
-            loadImageFromBuffer(buffer).then((img) => {
-              const texture = this._createTexture(img, item, resourceManager);
-              resourceManager.addContentRestorer(new Texture2DContentRestorer(texture, url, requestConfig));
+            loadImageFromBuffer(buffer).then((img) => {
+              const { texture, generateMipmap } = this._createTexture(img, item, resourceManager);
+              resourceManager.addContentRestorer(
+                new Texture2DContentRestorer(texture, url, requestConfig, generateMipmap)
+              );
               resolve(texture);
             }, reject);
@@
-  private _createTexture(img: HTMLImageElement, item: LoadItem, resourceManager: ResourceManager): Texture2D {
+  private _createTexture(
+    img: HTMLImageElement,
+    item: LoadItem,
+    resourceManager: ResourceManager
+  ): { texture: Texture2D; generateMipmap: boolean } {
@@
-    return texture;
+    return { texture, generateMipmap };
   }
 }
 
 class Texture2DContentRestorer extends ContentRestorer<Texture2D> {
   constructor(
     resource: Texture2D,
     public url: string,
-    public requestConfig: RequestConfig
+    public requestConfig: RequestConfig,
+    private readonly generateMipmap: boolean = false
   ) {
     super(resource);
   }
@@
           } else {
             return loadImageFromBuffer(buffer).then((img) => {
               texture.setImageSource(img);
-              texture.generateMipmaps();
+              this.generateMipmap && texture.generateMipmaps();
               return texture;
             });
           }

Also applies to: 60-96, 108-127

🤖 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 `@packages/loader/src/Texture2DLoader.ts` around lines 49 - 52, The loader
currently discards the original mipmap decision and always regenerates mipmaps
during restore; update the flow so the mipmap boolean chosen at load time is
stored and reused by the restorer. Concretely: when creating the texture in
loadImageFromBuffer and other load paths (calls to _createTexture and
instantiation of Texture2DContentRestorer), capture the computed mipmap flag
(from supportGenerateMipmapsWithCorrection(...) or explicit mipmap option) and
pass that flag into Texture2DContentRestorer. Then change
Texture2DContentRestorer.restoreContent to use the stored mipmap flag instead of
recomputing/generating mipmaps unconditionally. Ensure all places that add a
Texture2DContentRestorer (the other load branches) are updated to include this
stored flag.
tests/src/core/audio/AudioSourcePendingPlayback.test.ts-90-109 (1)

90-109: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore window.AudioContext in teardown.

beforeEach() overwrites the global constructor, but afterEach() never puts it back. That leaks the mock into later suites and makes them order-dependent.

🤖 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 `@tests/src/core/audio/AudioSourcePendingPlayback.test.ts` around lines 90 -
109, The test setup overrides the global constructor by assigning
MockAudioContext to window.AudioContext in beforeEach but never restores it;
capture the original constructor (e.g., save window.AudioContext to a variable
like originalAudioContext at top or at start of beforeEach) and restore it in
afterEach by reassigning window.AudioContext = originalAudioContext (or delete
window.AudioContext if it was undefined originally) so the mock does not leak
into other suites; update the afterEach alongside the existing
vi.restoreAllMocks()/useRealTimers() cleanup to perform this restore.
packages/core/src/clone/CloneManager.ts-165-194 (1)

165-194: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Deep-cloning Map/Set still aliases nested mutable entries.

These branches only allocate a new container. Non-_remap keys/values are copied by reference, so CloneMode.Deep still shares nested objects inside Map and Set.

🤖 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 `@packages/core/src/clone/CloneManager.ts` around lines 165 - 194, The Map/Set
branches only allocate new containers but still copy nested objects by reference
when they don't implement _remap, so CloneMode.Deep doesn't actually deep-clone
entries; modify the Map (targetPropertyM) and Set (targetPropertyS) loops to,
for each entry where value or key is an object and does NOT have a _remap,
invoke the same recursive cloning routine used elsewhere in CloneManager (the
per-property deep-clone function used with srcRoot/targetRoot and
CloneMode.Deep) to produce a cloned key/value before inserting into the new
Map/Set, while keeping the existing _remap path (i.e., prefer
(<ICustomClone>...)._remap(srcRoot, targetRoot) when present).
packages/core/src/animation/internal/AnimatorLayerData.ts-15-17 (1)

15-17: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a null-prototype map for state names.

animatorStateDataMap is now keyed by arbitrary state names, but {} makes keys like __proto__ and constructor special. A user-authored controller can corrupt lookups here.

🛡️ Suggested fix
-  animatorStateDataMap: Record<string, AnimatorStateData> = {};
+  animatorStateDataMap: Record<string, AnimatorStateData> = Object.create(null);
🤖 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 `@packages/core/src/animation/internal/AnimatorLayerData.ts` around lines 15 -
17, animatorStateDataMap is currently initialized with {} which allows prototype
keys like "__proto__" to collide; update the AnimatorLayerData implementation so
animatorStateDataMap uses a null-prototype object or an actual Map to avoid
prototype pollution: replace the current Record<string, AnimatorStateData> = {}
initialization with either Record<string, AnimatorStateData> =
Object.create(null) (keeping the same type) or change the field to Map<string,
AnimatorStateData> and initialize it with new Map() and update any
lookup/assignment code that references animatorStateDataMap accordingly.
packages/core/src/clone/CloneManager.ts-108-139 (1)

108-139: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep explicit clone decorators authoritative.

@ignoreClone no longer wins for remappable refs because _remap runs first, and Lines 128-138 can upgrade @shallowClone into deep cloning. That breaks the documented contract of both decorators.

✂️ Suggested fix
-    // 1. Remappable references (Entity/Component) are always remapped, highest priority
-    if (sourceProperty instanceof Object && (<ICustomClone>sourceProperty)._remap) {
-      target[k] = (<ICustomClone>sourceProperty)._remap(srcRoot, targetRoot);
-      return;
-    }
-
-    // 2. Explicit ignore
+    // 1. Explicit ignore must stay authoritative
     if (cloneMode === CloneMode.Ignore) return;
 
-    // 3. Primitives / null / undefined - direct assign
+    // 2. Remappable references (Entity/Component)
+    if (sourceProperty instanceof Object && (<ICustomClone>sourceProperty)._remap) {
+      target[k] = (<ICustomClone>sourceProperty)._remap(srcRoot, targetRoot);
+      return;
+    }
+
+    // 3. Primitives / null / undefined - direct assign
     if (!(sourceProperty instanceof Object)) {
       target[k] = sourceProperty;
       return;
     }
@@
-    } else if (effectiveCloneMode !== CloneMode.Assignment) {
-      // Decorated Shallow/Deep: upgrade to Deep if target already has independent same-type instance.
-      // Assignment is never upgraded — it means the user explicitly wants a reference copy.
-      const targetProperty = target[k];
-      if (
-        targetProperty &&
-        targetProperty !== sourceProperty &&
-        targetProperty.constructor === sourceProperty.constructor
-      ) {
-        effectiveCloneMode = CloneMode.Deep;
-      }
     }
🤖 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 `@packages/core/src/clone/CloneManager.ts` around lines 108 - 139, The current
logic upgrades a decorated shallow clone into deep because the upgrade branch
checks effectiveCloneMode !== CloneMode.Assignment rather than whether the
cloneMode was explicitly provided; to fix this, make explicit decorators
authoritative by only performing the "upgrade to Deep when target already has
independent same-type instance" logic when the incoming cloneMode was not
explicitly provided (i.e., cloneMode === undefined / inferred). Concretely, in
CloneManager (the block that sets effectiveCloneMode and then checks
targetProperty to maybe set effectiveCloneMode = CloneMode.Deep), change the
condition so the upgrade runs only when the original cloneMode parameter was
undefined (use that original flag or a local isExplicit := cloneMode !==
undefined) and keep Assignment behavior unchanged; reference symbols: cloneMode,
effectiveCloneMode, CloneMode, CloneManager._inferCloneMode, sourceProperty,
target[k], and _remap.
packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts-308-321 (1)

308-321: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the optional components array.

IBasicEntity.components is optional in the schema. _addComponents() currently assumes it is always present and will throw on components.length for valid files that omit an empty component list.

Suggested fix
   private _addComponents(entity: Entity, components: IEntity["components"]): void {
+    if (!components?.length) {
+      return;
+    }
+
     const context = this.context;
     const componentMap = context.components;
     const componentConfigMap = context.componentConfigMap;
🤖 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 `@packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts`
around lines 308 - 321, The _addComponents method assumes components is always
present and will throw when IEntity["components"] is undefined; add a guard at
the top of _addComponents(entity: Entity, components: IEntity["components"]) to
return early if !components or components.length === 0. Locate the function
named _addComponents and add the check before using componentMap,
componentConfigMap, Loader.getClass(...) and calling this._addComponentPlugin so
the rest of the loop only runs when a valid components array is provided.
packages/loader/src/HDRDecoder.ts-182-189 (1)

182-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strip \r when reading HDR header lines.

_readStringLine() currently keeps carriage returns, so parseHeader() will reject valid CRLF-terminated HDR files: "FORMAT=32-bit_rle_rgbe\r" no longer matches, and the blank separator line is never seen as empty.

Suggested fix
   private static _readStringLine(uint8array: Uint8Array, startIndex: number): string {
     let line = "";
     for (let i = startIndex, n = uint8array.length; i < n; i++) {
       const character = String.fromCharCode(uint8array[i]);
       if (character === "\n") break;
-      line += character;
+      if (character !== "\r") {
+        line += character;
+      }
     }
     return line;
   }
🤖 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 `@packages/loader/src/HDRDecoder.ts` around lines 182 - 189, _readStringLine
currently preserves carriage returns which breaks parseHeader's detection of
lines (e.g., "FORMAT=32-bit_rle_rgbe\r") and blank separator lines; update
HDRDecoder._readStringLine to not include '\r' characters when accumulating the
line (or trim a trailing '\r' before returning) so lines returned to parseHeader
are normalized and CRLF-terminated files are accepted. Ensure the change affects
the HDRDecoder._readStringLine method only and that parseHeader will see an
empty string for a CRLF blank separator.
packages/core/src/Entity.ts-572-590 (1)

572-590: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset _isActiveChanging in a finally block.

_setActiveInHierarchy() and _setInActiveInHierarchy() can run user code through component/script enable/disable hooks. If one of those throws, this flag stays true forever and every later active-state change on the entity will fail immediately.

Suggested fix
   _processActive(activeChangeFlag: ActiveChangeFlag): void {
     if (this._isActiveChanging) {
       throw "Note: can't set the 'main inActive entity' active in hierarchy, if the operation is in main inActive entity or it's children script's onDisable Event.";
     }
-    this._isActiveChanging = true;
-    this._setActiveInHierarchy(activeChangeFlag);
-    this._isActiveChanging = false;
+    this._isActiveChanging = true;
+    try {
+      this._setActiveInHierarchy(activeChangeFlag);
+    } finally {
+      this._isActiveChanging = false;
+    }
   }
@@
   _processInActive(activeChangeFlag: ActiveChangeFlag): void {
     if (this._isActiveChanging) {
       throw "Note: can't set the 'main active entity' inActive in hierarchy, if the operation is in main active entity or it's children script's onEnable Event.";
     }
-    this._isActiveChanging = true;
-    this._setInActiveInHierarchy(activeChangeFlag);
-    this._isActiveChanging = false;
+    this._isActiveChanging = true;
+    try {
+      this._setInActiveInHierarchy(activeChangeFlag);
+    } finally {
+      this._isActiveChanging = false;
+    }
   }
🤖 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 `@packages/core/src/Entity.ts` around lines 572 - 590, Both _processActive and
_processInActive leave this._isActiveChanging set to true if
_setActiveInHierarchy/_setInActiveInHierarchy throws; wrap the call and the flag
reset in a try/finally so this._isActiveChanging is always set back to false
(and rethrow the original error after finally). Modify the bodies of
_processActive and _processInActive to set this._isActiveChanging = true before
calling _setActiveInHierarchy/_setInActiveInHierarchy, call them inside a try,
and reset this._isActiveChanging = false in finally to guarantee cleanup even on
exceptions.
packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts-21-38 (1)

21-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await transform mutations before resolving parseEntity.

parsePropsAndMethods() is async, but this path drops its promise and returns the Entity immediately. Any async transform prop/method resolution can still be running while callers continue wiring the hierarchy, so initialization order becomes nondeterministic.

Suggested fix
-  parseEntity(entityConfig: IEntity): Promise<Entity> {
-    return this._getEntityByConfig(entityConfig).then((entity) => {
+  parseEntity(entityConfig: IEntity): Promise<Entity> {
+    return this._getEntityByConfig(entityConfig).then(async (entity) => {
       entity.isActive = entityConfig.isActive ?? true;
       const transform = entity.transform;
       const transformConfig = entityConfig.transform;
       if (transformConfig) {
-        this.parsePropsAndMethods(transform, transformConfig);
+        await this.parsePropsAndMethods(transform, transformConfig);
       } else {
         const { position, rotation, scale } = entityConfig;
         if (position) transform.position.copyFrom(position);
         if (rotation) transform.rotation.copyFrom(rotation);
         if (scale) transform.scale.copyFrom(scale);
       }
       entity.layer = entityConfig.layer ?? entity.layer;
       // `@ts-ignore`
       this._context.type === ParserType.Prefab && entity._markAsTemplate(this._context.resource);
       return entity;
     });
   }
🤖 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
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`
around lines 21 - 38, parseEntity returns before async transform mutations
complete because parsePropsAndMethods is called but not awaited; update
parseEntity (the method that calls this._getEntityByConfig(...).then(...)) to
await or return the promise from parsePropsAndMethods when transformConfig
exists so the Entity is only resolved after parsePropsAndMethods finishes.
Ensure the change preserves setting entity.isActive, applying fallback
position/rotation/scale, assigning entity.layer, and still calls
entity._markAsTemplate(this._context.resource) when this._context.type ===
ParserType.Prefab.
packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts-65-79 (1)

65-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply Layer.Layer0 explicitly instead of truthiness-checking it.

layer is numeric, so Layer0 is 0. The current check skips valid overrides back to layer 0, which breaks prefab/scene modifications when the existing entity is already on a non-zero layer.

Suggested fix
-    if (entityConfig.layer) entity.layer = entityConfig.layer;
+    if (entityConfig.layer !== undefined) entity.layer = entityConfig.layer;
🤖 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 `@packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts`
around lines 65 - 79, In _applyEntityData, the code uses truthiness to decide
whether to set entity.layer (if (entityConfig.layer) ...), which skips valid
numeric layer 0; change the guard to check for undefined (e.g. if
(entityConfig.layer !== undefined) entity.layer = entityConfig.layer) so a
config value of Layer.Layer0 (0) is applied to Entity.layer; update the
condition in the _applyEntityData method referencing entityConfig.layer and
Entity.layer accordingly.
packages/loader/src/gltf/parser/GLTFSceneParser.ts-199-201 (1)

199-201: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include the skinned-mesh node transform in the fallback bounds conversion.

mesh.bounds are in the renderer entity's local space, not in rootBone space. Using only inverse(rootBone.worldMatrix) drops the skinned-mesh node transform, so localBounds becomes wrong whenever rootBone is outside bones and the renderer node is not identity relative to it.

Suggested fix
     } else {
+      const toRootBoneSpace = new Matrix();
       const inverseRootBoneWorld = new Matrix();
       Matrix.invert(rootBone.transform.worldMatrix, inverseRootBoneWorld);
-      BoundingBox.transform(mesh.bounds, inverseRootBoneWorld, skinnedMeshRenderer.localBounds);
+      Matrix.multiply(inverseRootBoneWorld, skinnedMeshRenderer.entity.transform.worldMatrix, toRootBoneSpace);
+      BoundingBox.transform(mesh.bounds, toRootBoneSpace, skinnedMeshRenderer.localBounds);
     }
🤖 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 `@packages/loader/src/gltf/parser/GLTFSceneParser.ts` around lines 199 - 201,
The current fallback uses only inverseRootBoneWorld
(Matrix.invert(rootBone.transform.worldMatrix)) which ignores the skinned-mesh
node's world transform and yields wrong localBounds; compute a combined
transform that maps the renderer's local space into the rootBone local space
(i.e., first get the skinned-mesh node's world matrix from the renderer node,
multiply it by inverseRootBoneWorld or multiply inverseRootBoneWorld by the node
world depending on matrix convention), then pass that combined matrix to
BoundingBox.transform(mesh.bounds, combinedMatrix,
skinnedMeshRenderer.localBounds) so the renderer node transform is included when
converting mesh.bounds.
packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts-251-275 (1)

251-275: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the full prefab ref through and register the nested dependency.

IRefEntity carries key and isClone, but this path only forwards { url }, so nested prefab/GLTF subresource refs no longer resolve the same way as ReflectionParser._getEntityByConfig(). This branch also never associates the loaded PrefabResource/GLTFResource with the owning prefab resource, so nested asset lifetime tracking is lost.

Suggested fix
-        .getResourceByRef<Entity>({
-          url: assetUrl
-        })
+        .getResourceByRef({
+          url: assetUrl,
+          key: entityConfig.key,
+          isClone: entityConfig.isClone
+        })
         .then((prefabResource: PrefabResource | GLTFResource) => {
+          if (this.context.type === ParserType.Prefab) {
+            // `@ts-ignore`
+            this.context.resource._addDependenceAsset(prefabResource);
+          }
           const entity =
             prefabResource instanceof PrefabResource
               ? prefabResource.instantiate()
🤖 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 `@packages/loader/src/resource-deserialize/resources/parser/HierarchyParser.ts`
around lines 251 - 275, The _parsePrefab path currently calls
engine.resourceManager.getResourceByRef with only { url: assetUrl }, losing
IRefEntity fields (key, isClone) and failing to register the loaded
PrefabResource/GLTFResource as a nested dependency; update the call in
_parsePrefab to pass the full IRefEntity (entityConfig) so nested refs resolve
the same way as ReflectionParser._getEntityByConfig, and after the resource is
loaded associate/link it with the owning prefab resource (using your resource
manager's dependency/link API) so lifetime tracking is preserved and nested
prefab/GLTF subresources are registered (keep the rest of the logic that
instantiates, creates ParserContext, sets _prefabContextMap and resolves
_prefabPromiseMap).
packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts-48-73 (1)

48-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix image-backed Texture2D decode: reject on Image failure, revoke object URLs, and cache in _objectPool.

  • The image-backed path never attaches img.onerror, so the AssetPromise can hang forever on a broken base/mip image.
  • URL.createObjectURL(...) is never revoked (leaks).
  • The successful decode is not registered into engine.resourceManager._objectPool[url], unlike the pixel-buffer branch, so it can’t be found later by URL.
Suggested fix
       } else {
-        const blob = new window.Blob([imagesData[0]]);
-        const img = new Image();
-        img.onload = () => {
-          texture2D.setImageSource(img);
-          let completedCount = 0;
-          const onComplete = () => {
-            completedCount++;
-            if (completedCount >= mipCount) {
-              resolve(texture2D);
-            }
-          };
-          onComplete();
-          if (mipmap) {
-            texture2D.generateMipmaps();
-            for (let i = 1; i < mipCount; i++) {
-              const blob = new window.Blob([imagesData[i]]);
-              const img = new Image();
-              img.onload = () => {
-                texture2D.setImageSource(img, i);
-                onComplete();
-              };
-              img.src = URL.createObjectURL(blob);
-            }
-          }
-        };
-        img.src = URL.createObjectURL(blob);
+        const loadImage = (data: Uint8Array) =>
+          new Promise<HTMLImageElement>((resolveImage, rejectImage) => {
+            const objectURL = URL.createObjectURL(new window.Blob([data]));
+            const img = new Image();
+            img.onload = () => {
+              URL.revokeObjectURL(objectURL);
+              resolveImage(img);
+            };
+            img.onerror = (e) => {
+              URL.revokeObjectURL(objectURL);
+              rejectImage(e);
+            };
+            img.src = objectURL;
+          });
+
+        loadImage(imagesData[0])
+          .then((baseImage) => {
+            texture2D.setImageSource(baseImage);
+            if (mipmap) {
+              texture2D.generateMipmaps();
+            }
+            return Promise.all(
+              imagesData.slice(1).map((data, index) =>
+                loadImage(data).then((mipImage) => texture2D.setImageSource(mipImage, index + 1))
+              )
+            );
+          })
+          .then(() => {
+            // `@ts-ignore`
+            engine.resourceManager._objectPool[url] = texture2D;
+            resolve(texture2D);
+          }, reject);
       }
🤖 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
`@packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts`
around lines 48 - 73, The image-backed decode in TextureDecoder must handle
image load errors, revoke created object URLs, and cache the finished Texture2D
in engine.resourceManager._objectPool keyed by the source URL; update the img
handling in the block that creates Image() (base and mip images) to attach
img.onerror to call reject(err) on the surrounding AssetPromise (or pass an
Error describing which mip failed), call URL.revokeObjectURL(img.src) in both
onload and onerror, and after the final onComplete resolves, store the texture
into engine.resourceManager._objectPool[url] (same key used by the pixel-buffer
branch); ensure the mip loop also revokes each created blob URL on load/error
and propagates failures to reject so the promise never hangs.
packages/core/src/animation/Animator.ts-492-520 (1)

492-520: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clip-change listeners are never unsubscribed.

At Line 519, listeners are registered on each state data build, but there is no matching unsubscribe in reset/destroy paths (Line 336-Line 354, Line 367-Line 374). This can leak closures and trigger duplicate handler rebuilds over time.

Also applies to: 336-354, 367-374

🤖 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 `@packages/core/src/animation/Animator.ts` around lines 492 - 520, The
clipChangedListener created in _saveAnimatorEventHandlers is added to
state._updateFlagManager but never removed, causing leaks and duplicate
rebuilds; modify _saveAnimatorEventHandlers to return the created listener (or
store it on AnimatorState/AnimatorStateData, e.g. attach to
animatorStateData._clipChangedListener) and ensure reset/destroy paths (methods
handling state teardown referenced around lines handling reset/destroy) call
state._updateFlagManager.removeListener(listener) (or use that stored reference)
when the state is cleared/destroyed so the listener is unsubscribed and closures
are released.
packages/core/src/animation/Animator.ts-232-242 (1)

232-242: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

findAnimatorState can return the wrong state play-data.

At Line 240-Line 241, if the target state exists but is not current srcPlayData/destPlayData, the method still returns srcPlayData (which may belong to a different state). This can silently mutate/read the wrong state instance.

💡 Suggested fix
   findAnimatorState(stateName: string, layerIndex: number = -1): AnimatorStatePlayData {
     const { state, layerIndex: foundLayer } = this._getAnimatorStateInfo(stateName, layerIndex);
     if (!state || foundLayer < 0) return null;
     const layerData = this._animatorLayersData[foundLayer];
     if (!layerData) return null;
-    // Check srcPlayData and destPlayData for the matching state
+    // Check srcPlayData and destPlayData for the matching state
     if (layerData.srcPlayData.state === state) return layerData.srcPlayData;
     if (layerData.destPlayData.state === state) return layerData.destPlayData;
-    // State exists in controller but not currently playing — return srcPlayData initialized with the state
-    return layerData.srcPlayData;
+    return null;
   }
🤖 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 `@packages/core/src/animation/Animator.ts` around lines 232 - 242,
findAnimatorState currently returns layerData.srcPlayData when the requested
state exists in the controller but isn’t equal to srcPlayData or destPlayData,
which can return the wrong instance; update findAnimatorState to, after
obtaining { state, layerIndex: foundLayer } from _getAnimatorStateInfo and
validating layerData, if neither layerData.srcPlayData.state nor
layerData.destPlayData.state matches state, create and return a new
AnimatorStatePlayData (or a properly initialized play-data object) whose .state
is the found state (do not return the existing srcPlayData instance), ensuring
you use the AnimatorStatePlayData constructor/initialization used elsewhere and
preserve null checks for state and layerData.
🟡 Minor comments (4)
packages/loader/src/SpriteLoader.ts-35-35 (1)

35-35: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Type inference for getResourceByRef isn’t possible without explicit generics

getResourceByRef<T extends EngineObject>(ref: { url: string; ... }): AssetPromise<T> can’t infer T from the ref argument because T only appears in the return type. In packages/loader/src/SpriteLoader.ts (lines 35 and 47), the calls are also preceded by // @ts-ignore``, so the annotated .then((atlas: SpriteAtlas) => ...) / `.then((texture: Texture2D) => ...)` isn’t enforced by the actual return type—reintroduce explicit generics or adjust the API typing to make the returned asset type safe.

🤖 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 `@packages/loader/src/SpriteLoader.ts` at line 35, Calls to getResourceByRef
lack an explicit generic so TypeScript can’t infer the returned AssetPromise
type; update the SpriteLoader usage to call getResourceByRef with an explicit
type parameter (e.g., getResourceByRef<SpriteAtlas>(...) and
getResourceByRef<Texture2D>(...)) and remove the surrounding // `@ts-ignore` so
the .then callbacks (atlas: SpriteAtlas / texture: Texture2D) are type-checked,
or alternatively adjust the getResourceByRef signature to surface the asset
generic from the ref argument so callers don’t need to specify generics.
tests/src/loader/SceneParser.test.ts-4-5 (1)

4-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Skipped placeholder test provides no regression signal.

The new suite currently adds no active coverage. Please either implement this test now or convert it to it.todo(...) and track it outside this PR.

If you want, I can draft a concrete test case for the deleted-component reference path and propose assertions.

🤖 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 `@tests/src/loader/SceneParser.test.ts` around lines 4 - 5, The skipped
placeholder test "should load project successfully with component be deleted" in
tests/src/loader/SceneParser.test.ts provides no coverage; either implement the
test body for the deleted-component reference path inside the existing test
(replace it.skip with it and add assertions that SceneParser loads the project
and handles deleted components gracefully), or change it.skip to it.todo("should
load project successfully with component be deleted") so CI records the pending
test; look for the test identifier string and the use of it.skip to locate and
update the test accordingly.
packages/loader/src/resource-deserialize/resources/schema/MaterialSchema.ts-93-95 (1)

93-95: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add boolean to IMaterialSchema.shaderData[*].value to match MaterialLoaderType.Boolean.

MaterialLoaderType includes Boolean, MaterialLoader.ts consumes it as value ? 1 : 0, but IMaterialSchema.shaderData[*].value omits boolean, rejecting valid boolean shader payloads.

🔧 Proposed fix
   shaderData: {
     [key: string]: {
       type: MaterialLoaderType;
-      value: IVector3 | IVector2 | IColor | number | IAssetRef;
+      value: IVector3 | IVector2 | IColor | number | boolean | IAssetRef;
     };
   };
🤖 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 `@packages/loader/src/resource-deserialize/resources/schema/MaterialSchema.ts`
around lines 93 - 95, The IMaterialSchema currently defines shaderData[*].value
without boolean, but MaterialLoaderType includes Boolean and MaterialLoader.ts
expects booleans (it uses value ? 1 : 0); update the IMaterialSchema type for
shaderData entries so that the value union adds boolean (e.g., include "boolean"
alongside IVector3 | IVector2 | IColor | number | IAssetRef) so boolean shader
payloads validate correctly; adjust any associated types or tests that rely on
IMaterialSchema.shaderData to accept the new boolean variant.
packages/loader/src/Texture2DLoader.ts-19-29 (1)

19-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Revoke the blob URL on failed image decodes.

Line 27 rejects without releasing the createObjectURL() result, so repeated bad/aborted image loads leak blob URLs in the page.

♻️ Suggested fix
 function loadImageFromBuffer(buffer: ArrayBuffer): AssetPromise<HTMLImageElement> {
   return new AssetPromise((resolve, reject) => {
     const blob = new Blob([buffer]);
     const img = new Image();
+    const objectURL = URL.createObjectURL(blob);
     img.onload = () => {
-      URL.revokeObjectURL(img.src);
+      URL.revokeObjectURL(objectURL);
       resolve(img);
     };
-    img.onerror = reject;
-    img.src = URL.createObjectURL(blob);
+    img.onerror = (e) => {
+      URL.revokeObjectURL(objectURL);
+      reject(e);
+    };
+    img.src = objectURL;
   });
 }
🤖 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 `@packages/loader/src/Texture2DLoader.ts` around lines 19 - 29, The
loadImageFromBuffer function leaks blob URLs on failed decodes because the
img.onerror handler calls reject without revoking the URL; update
loadImageFromBuffer so that both img.onload and img.onerror revoke
URL.revokeObjectURL(img.src) before resolving/rejecting (and pass the
error/event into reject), ensuring the blob created for img.src is always
released; reference the img.onload, img.onerror handlers and URL.revokeObjectURL
usage in loadImageFromBuffer to implement this change.
🧹 Nitpick comments (1)
tests/src/loader/StrippedEntity.test.ts (1)

67-128: ⚡ Quick win

Guard test cleanup with try/finally to avoid cache leakage on assertion failures.

At Line 127/204/275, cleanup runs only on success. Wrap each test body after cachePrefab(...) in try/finally so removeCachedPrefab(...) and root.destroy() always execute.

💡 Suggested pattern
-    const nestedPrefab = await PrefabParser.parse(engine, "char.prefab", nestedPrefabData);
-    cachePrefab("char.prefab", nestedPrefab);
-    ...
-    const outerPrefab = await PrefabParser.parse(engine, "outer.prefab", outerPrefabData);
-    const root = outerPrefab.instantiate();
-    ...
-    root.destroy();
-    removeCachedPrefab("char.prefab");
+    const nestedPrefab = await PrefabParser.parse(engine, "char.prefab", nestedPrefabData);
+    cachePrefab("char.prefab", nestedPrefab);
+    let root;
+    try {
+      ...
+      const outerPrefab = await PrefabParser.parse(engine, "outer.prefab", outerPrefabData);
+      root = outerPrefab.instantiate();
+      ...
+    } finally {
+      root?.destroy();
+      removeCachedPrefab("char.prefab");
+    }

Also applies to: 139-205, 216-276

🤖 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 `@tests/src/loader/StrippedEntity.test.ts` around lines 67 - 128, The test can
leak cached prefabs and not destroy instantiated roots on assertion failure;
wrap the test body that begins after cachePrefab("char.prefab", ...) in a
try/finally so that removeCachedPrefab("char.prefab") and root.destroy() always
run; locate the block around PrefabParser.parse/outerPrefab.instantiate() and
ensure the finally calls root.destroy() (guarding if root is defined) and
removeCachedPrefab("char.prefab") so cleanup happens even when expectations
fail; apply the same try/finally pattern to the other similar tests that call
cachePrefab/removeCachedPrefab (the blocks referenced at the other ranges).

Comment thread packages/loader/src/prefab/PrefabResource.ts Outdated
GuoLei1990

This comment was marked as outdated.

- Remove dead parseProperty/renderState in MaterialLoader
- Restore revokeObjectURL in GLTFUtils.loadImageBuffer
- Add null guard in Animator._crossFade
- Guard playSpeed=0 division in _updatePlayingState
- Clean up clipChangedListener in Animator._reset
GuoLei1990

This comment was marked as outdated.

luzhuang added 2 commits May 26, 2026 11:26
Revert non-gltf loader changes and restore misdeleted
files back to dev/2.0 state.
…ture

AssetType enum has no Texture2D member; this was an incorrect
change from fix/shaderlab.
@luzhuang luzhuang changed the title fix(loader, animation, physics): cherry-pick fixes from fix/shaderlab fix(animation, physics, audio, gltf): cherry-pick fixes from fix/shaderlab May 26, 2026
GuoLei1990

This comment was marked as outdated.

Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/animation/Animator.ts (1)

238-242: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback returns play data for a different state than requested.

When the requested state exists in the controller but is not currently playing in srcPlayData or destPlayData, the fallback at line 241 returns srcPlayData which may contain a completely different state. The JSDoc promises "Per-instance play data for a state by name" but this can return play data for the wrong state.

If caller requests state "Run" while "Walk" is playing, srcPlayData.state is "Walk" but we return it as the result for "Run".

Consider returning null when the state is not currently active, consistent with the JSDoc's "or null if not found" contract:

   if (layerData.srcPlayData.state === state) return layerData.srcPlayData;
   if (layerData.destPlayData.state === state) return layerData.destPlayData;
-  // State exists in controller but not currently playing — return srcPlayData initialized with the state
-  return layerData.srcPlayData;
+  // State exists in controller but not currently playing
+  return null;
🤖 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 `@packages/core/src/animation/Animator.ts` around lines 238 - 242, The fallback
in Animator.ts currently returns layerData.srcPlayData when neither
layerData.srcPlayData.state nor layerData.destPlayData.state matches the
requested state, which can return play data for the wrong state; change the
method in the Animator class that queries per-instance play data (the code
referencing layerData.srcPlayData, layerData.destPlayData, srcPlayData.state and
destPlayData.state) to return null instead of returning srcPlayData when no
active playData matches the requested state, and update the method's return
handling/comments to reflect the "or null if not found" contract so callers can
detect a missing active state.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@packages/core/src/animation/Animator.ts`:
- Around line 238-242: The fallback in Animator.ts currently returns
layerData.srcPlayData when neither layerData.srcPlayData.state nor
layerData.destPlayData.state matches the requested state, which can return play
data for the wrong state; change the method in the Animator class that queries
per-instance play data (the code referencing layerData.srcPlayData,
layerData.destPlayData, srcPlayData.state and destPlayData.state) to return null
instead of returning srcPlayData when no active playData matches the requested
state, and update the method's return handling/comments to reflect the "or null
if not found" contract so callers can detect a missing active state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 03b05a77-20b0-4a05-a92d-3b96c56f8486

📥 Commits

Reviewing files that changed from the base of the PR and between 618d4dd and 0375f07.

📒 Files selected for processing (6)
  • packages/core/src/animation/Animator.ts
  • packages/core/src/animation/internal/AnimatorStateData.ts
  • tests/src/core/particle/ParticleTextureSheetAnimation.test.ts
  • tests/src/core/particle/RateOverTimeReplay.test.ts
  • tests/src/loader/PrefabResource.test.ts
  • tests/src/loader/RenderTargetLoader.test.ts
✅ Files skipped from review due to trivial changes (3)
  • tests/src/loader/RenderTargetLoader.test.ts
  • tests/src/core/particle/RateOverTimeReplay.test.ts
  • tests/src/loader/PrefabResource.test.ts

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 mentioned this pull request Jun 1, 2026
3 tasks
Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

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

总结

从 fix/shaderlab 摘出 animation/physics/audio/gltf 四个子系统的修复,收窄成独立 PR。physics(teleport-vs-sweep 拆 _syncEntityTransformToNative、Collision normal 方向、PhysX kinematic CCD)、audio(前后台生命周期 + pending playback)、gltf(skinned bounds 用 rootBone 逆世界矩阵、LCA 含 skinned-mesh 节点)这三块方向合理、基本 self-contained。阻塞项集中在 animation,外加两个非 animation 的 scope 问题。

问题

P0

  • physics-physx/package.json / physics-lite/package.json 版本被降级 2.0.0-alpha.330.0.0-experimental-2.0-game.14(已核对 HEAD,physx 第 3 行就是)。这是从 game 分支 cherry-pick 带进来的产物,绝不能合进 dev/2.0。改回 alpha 版本。

P1

  • Animator.ts:241findAnimatorState 的 fallback 返回错误 state 的 playData

    if (layerData.srcPlayData.state === state) return layerData.srcPlayData;
    if (layerData.destPlayData.state === state) return layerData.destPlayData;
    // State exists in controller but not currently playing — return srcPlayData initialized with the state
    return layerData.srcPlayData;   // ← bug

    查询的 state 存在但未在播时,返回的是当前正在播的 srcPlayData,它的 .state 是另一个动画。注释说 "srcPlayData initialized with the state" 与代码不符——根本没初始化成查询的 state。调用方拿去改 .speed/.wrapMode 会静默作用到错误的动画上。应 return null

  • AnimatorStateMachine.removeStatedelete this._statesMap[name] 移到 if (index > -1) 块外,且不再 null defaultState

    拿一个 stale stateindex===-1)调用时,仍会把 map 里同名的新 state删掉。加上 defaultState 类型从 AnimatorState | null 改成非空但 removeState 不再清它 → 悬挂 default 隐患。

  • 自/活跃-dest crossFade 的 no-op 守卫被删除,且无替代测试

    老代码里 if srcPlayData/destPlayData.state === crossState return false 的守卫连同 3 个测试一起删了,没有补充测试说明新行为。如果是有意对齐 Unity,请补一个行为测试或在描述里标 breaking change。

  • 测试质量两条(链路 bug 必须用公开 API 测):

    • Animator.test.ts "find animator state":expect(animatorState).to.eq(currentAnimatorState)findAnimatorState(返回 AnimatorStatePlayData)和 getCurrentAnimatorState(返回 AnimatorState)比,类型不同永远不等。应比 .state
    • crossfade-speed 测试仍 reach 进 animator["_animatorLayersData"][0] 私有字段,而不是走它本该验证的 findAnimatorState 公开 API。把实现结构锁死进测试,且 revert findAnimatorState 修复后测试照样绿——没守住回归。

P2

  • Animator.ts:384-385 新增的 srcPlayData && ... / destPlayData && ... 空守卫是死代码 — 这俩现在 by construction 非空(AnimatorLayerData 急切初始化成 new AnimatorStatePlayData())。是本 PR 新引入的 + 行冗余,删掉。

  • findAnimatorState 上方有两个相邻的 /** */ JSDoc 块,只有第二个生效,删掉第一个。

  • PhysicsScene.test.ts 静默删了约 290 行 raycast/sweep filter-stack 测试。如果这些测试对应的生产代码确实没被 cherry-pick 进来,删除是合理的;但这是一大块静默的覆盖移除,请确认这些测试的被测对象确实不在 dev/2.0,并在描述里说明。(注意同文件里两个既有测试的断言从 not.toHaveBeenCalled 翻成 toHaveBeenCalled——那个是 kinematic-move→setKinematicTarget 修复带来的合理行为变更,已有注释,OK。)

P3

  • Collision.test.ts / DynamicCollider.test.ts 里有一批探索式测试(HYPOTHESIS:/ultrathink probe/R0:-R8:/cocos-parity probe,还有个 console.info 打 ratio 的),像开发期 scratchpad 而非沉淀的回归用例。建议清理成明确断言的回归测试。

注意(不是问题,是确认对了)

physics 的 _syncEntityTransformToNative 拆分是非对称的:Collider 基类给 teleport 语义,DynamicCollider override 把 kinematic-Target 走 move()CharacterController 只 override _teleportToEntityTransform(用 setWorldPosition、忽略 rotation)。每个子类确实不同——这是好模式,不是"对称改动"陷阱。唯一确认点:CharacterController 丢 rotation 是否有意。

自检

findAnimatorState:241physics-physx/package.json:3 版本降级均已对照 HEAD 0375f073f 实际源码确认。我 03:34/09:56 两轮 review 针对的就是当前 HEAD,所列 animation 项均未在 HEAD 修复。作者未对任何 inline 回复,无已解释项需排除。_syncEntityTransformToNative 非对称确认为正确模式、不作为问题列出。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants