Skip to content

fix(entity): clearChildren bug + setSiblingIndex without parent#2991

Open
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/entity-multiple-bugs
Open

fix(entity): clearChildren bug + setSiblingIndex without parent#2991
cptbtptpbcptdtptp wants to merge 2 commits into
galacean:dev/2.0from
cptbtptpbcptdtptp:fix/entity-multiple-bugs

Conversation

@cptbtptpbcptdtptp
Copy link
Copy Markdown
Collaborator

@cptbtptpbcptdtptp cptbtptpbcptdtptp commented May 11, 2026

Summary

Two small Entity bugs found during prefab/scene editing:

  1. clearChildren 缺失边界处理 — fix: ensure children-related state is cleaned up correctly when clearChildren is invoked. Adds regression test in Entity.test.ts.

  2. siblingIndex setter 在无父节点时报错_setSiblingIndex path assumed _parent exists when _isRoot is false; the new guard else if (this._parent) skips the operation rather than crashing.

Test Plan

  • New unit test in Entity.test.ts covers the clearChildren regression
  • Manual: orphan entity (no parent, not in scene root) can have siblingIndex set without error

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved error handling for entity hierarchy operations with a graceful warning instead of exception.
    • Enhanced scene loading process with more reliable destruction mechanism.
  • Tests

    • Updated entity test suite to verify behavior of child clearing operations and event dispatching.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Walkthrough

Entity hierarchy operations now emit modify events in a deterministic sequence. clearChildren() dispatches a single parent Child event before deactivating each child, then calls _setParentChange() after clearing the child's owner scene. The siblingIndex setter warns instead of throwing when assigned to non-hierarchy entities. SceneManager.loadScene() uses safe iteration during scene destruction. Tests validate modify event counts and sibling index resets.

Changes

Entity Hierarchy Modification Behavior

Layer / File(s) Summary
Logger Import
packages/core/src/Entity.ts
Added Logger import alongside EngineObject to enable warnings during hierarchy operations.
siblingIndex Setter Logic
packages/core/src/Entity.ts
Replaced exception with Logger.warn when setting siblingIndex on non-hierarchy entities; root vs non-root reordering and parent Child modify dispatch remain unchanged.
clearChildren Method Detachment Sequence
packages/core/src/Entity.ts
Restructured child detachment to reset sibling index, dispatch parent Child modify event before deactivation, clear child owner scene, then invoke child._setParentChange() after scene update; added comments documenting event ordering.
SceneManager Safe Scene Iteration
packages/core/src/SceneManager.ts
Changed loadScene() to iterate via getLoopArray() (SafeLoopArray) instead of getArray() when destroying previous scenes; destruction behavior preserved.
Test Coverage for Modify Events
tests/src/core/Entity.test.ts
Registered modify listeners on parent and children to assert parent receives exactly one Child modify event, each child receives exactly one Parent modify event, and all children have siblingIndex reset to -1.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A child now knows its place in the tree,
when parent clears the hierarchy.
Events fire in order, safe and sound—
no modification whilst loops are round!
Siblings scatter, their indices reset,
the cleanest detachment yet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the two main bug fixes: clearChildren behavior and siblingIndex setter when entity has no parent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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: 2

Caution

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

⚠️ Outside diff range comments (2)
packages/core/src/Entity.ts (1)

399-420: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: EntityModifyFlags.Child dispatched per child contradicts the new test (and the documented "single event for the whole clear" intent).

this._dispatchModify(EntityModifyFlags.Child, this) is inside the for loop, so the parent receives one Child event per child cleared. The new test asserts parentModifyCount[EntityModifyFlags.Child]).eq(1) (see tests/src/core/Entity.test.ts), which will fail whenever clearChildren() runs over more than one child (the test itself uses two children).

If the intent is a single bulk event per the test's comment ("a single Child modify event for the whole clear"), hoist the dispatch out of the loop. If the intent really is one event per child (more consistent with _removeFromParent/_addToChildrenList), the test assertion needs updating instead — but please pick one.

🛠️ Suggested fix (single bulk event, matches the test)
   clearChildren(): void {
     const children = this._children;
+    if (children.length === 0) return;
+    // Dispatch `Child` to the old parent before `_processInActive` (which unregisters
+    // UI listeners via `cleanRootCanvas`), so subscribers such as UICanvas can react
+    // to the hierarchy change while still attached. A single bulk event represents
+    // the entire clear operation.
+    this._dispatchModify(EntityModifyFlags.Child, this);
     for (let i = children.length - 1; i >= 0; i--) {
       const child = children[i];
       child._parent = null;
       child._siblingIndex = -1;
-      // Dispatch `Child` to the old parent before `_processInActive` (which unregisters
-      // UI listeners via `cleanRootCanvas`), so subscribers such as UICanvas can react
-      // to the hierarchy change while still attached.
-      this._dispatchModify(EntityModifyFlags.Child, this);

       let activeChangeFlag = ActiveChangeFlag.None;
       child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy);
       child._isActiveInScene && (activeChangeFlag |= ActiveChangeFlag.Scene);
       activeChangeFlag && child._processInActive(activeChangeFlag);

       Entity._traverseSetOwnerScene(child, null); // Must after child._processInActive().

       child._setParentChange();
     }
     children.length = 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/core/src/Entity.ts` around lines 399 - 420, In clearChildren(), move
the call to this._dispatchModify(EntityModifyFlags.Child, this) out of the
per-child loop so the parent emits a single bulk Child modify event for the
entire clear operation; preserve the existing child cleanup sequence (clearing
_parent/_siblingIndex, computing activeChangeFlag and calling
child._processInActive(activeChangeFlag), Entity._traverseSetOwnerScene(child,
null), and child._setParentChange()) for each child, then after the loop (before
or after children.length = 0) invoke
this._dispatchModify(EntityModifyFlags.Child, this) once to match the test and
documented behavior.
tests/src/core/Entity.test.ts (1)

419-425: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Stale assertion: lonely entity no longer throws on siblingIndex set.

This test predates the new orphan-entity behavior in Entity.siblingIndex (the Logger.warn path added in this PR). entityX has no parent and is not a root, so the setter now warns instead of throwing — expect(lonelyBadFn).to.throw() will fail.

Either assert on the warning (spy Logger.warn) or remove the throw expectation.

🛠️ Suggested fix
-      // thorw error when set lonely entity
-      const entityX = new Entity(engine, "entityX");
-      var lonelyBadFn = function () {
-        entityX.siblingIndex = 1;
-      };
-      expect(lonelyBadFn).to.throw();
+      // Lonely entity should warn (not throw) when siblingIndex is set
+      const entityX = new Entity(engine, "entityX");
+      const warnSpy = vi.spyOn(Logger, "warn").mockImplementation(() => {});
+      expect(() => { entityX.siblingIndex = 1; }).not.toThrow();
+      expect(warnSpy).toHaveBeenCalled();
+      warnSpy.mockRestore();

Note: this also requires importing Logger from @galacean/engine-core at the top of the file.

🤖 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/Entity.test.ts` around lines 419 - 425, The test currently
expects Entity.siblingIndex setter to throw for an orphan entity (entityX), but
the setter now warns instead; update the test to spy on Logger.warn (import
Logger from '@galacean/engine-core' at the top), set up a spy around
Logger.warn, call the setter (e.g., entityX.siblingIndex = 1), assert that
Logger.warn was called with an appropriate message, and restore the spy;
alternatively remove the expect(lonelyBadFn).to.throw() assertion and replace it
with the Logger.warn assertion referencing entityX and the siblingIndex setter.
🤖 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.

Inline comments:
In `@packages/core/src/Entity.ts`:
- Around line 214-224: The setter siblingIndex has an invalid if/else/else
chain; update the second branch to be an else if that checks for this._parent so
orphans fall through to the Logger.warn path: keep the this._isRoot branch that
calls this._setSiblingIndex(this._scene._rootEntities, value), change the middle
branch to else if (this._parent) which calls
this._setSiblingIndex(parent._children, value) and
parent._dispatchModify(EntityModifyFlags.Child, parent), and let the final else
call Logger.warn(`The entity ${this.name} is not in the hierarchy`).

In `@tests/src/core/Entity.test.ts`:
- Around line 332-353: Test expects Entity.clearChildren() to dispatch a single
EntityModifyFlags.Child event for the whole operation, but current
implementation emits Child inside the per-child loop; update the
Entity.clearChildren() method to accumulate removals and call the
modify/dispatch for EntityModifyFlags.Child exactly once after all children are
detached (while still emitting the Parent modify event per detached child and
resetting siblingIndex to -1 for each child); locate the dispatch call(s) in
Entity.clearChildren() and move/condense them so only one Child event is emitted
for the overall clear.

---

Outside diff comments:
In `@packages/core/src/Entity.ts`:
- Around line 399-420: In clearChildren(), move the call to
this._dispatchModify(EntityModifyFlags.Child, this) out of the per-child loop so
the parent emits a single bulk Child modify event for the entire clear
operation; preserve the existing child cleanup sequence (clearing
_parent/_siblingIndex, computing activeChangeFlag and calling
child._processInActive(activeChangeFlag), Entity._traverseSetOwnerScene(child,
null), and child._setParentChange()) for each child, then after the loop (before
or after children.length = 0) invoke
this._dispatchModify(EntityModifyFlags.Child, this) once to match the test and
documented behavior.

In `@tests/src/core/Entity.test.ts`:
- Around line 419-425: The test currently expects Entity.siblingIndex setter to
throw for an orphan entity (entityX), but the setter now warns instead; update
the test to spy on Logger.warn (import Logger from '@galacean/engine-core' at
the top), set up a spy around Logger.warn, call the setter (e.g.,
entityX.siblingIndex = 1), assert that Logger.warn was called with an
appropriate message, and restore the spy; alternatively remove the
expect(lonelyBadFn).to.throw() assertion and replace it with the Logger.warn
assertion referencing entityX and the siblingIndex setter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8edd7d34-fe19-44e8-8217-73c91be6bae2

📥 Commits

Reviewing files that changed from the base of the PR and between 1bc2b10 and 44e7257.

📒 Files selected for processing (3)
  • packages/core/src/Entity.ts
  • packages/core/src/SceneManager.ts
  • tests/src/core/Entity.test.ts

Comment on lines 214 to 224
set siblingIndex(value: number) {
if (this._siblingIndex === -1) {
throw `The entity ${this.name} is not in the hierarchy`;
}

if (this._isRoot) {
this._setSiblingIndex(this._scene._rootEntities, value);
} else {
const parent = this._parent;
this._setSiblingIndex(parent._children, value);
parent._dispatchModify(EntityModifyFlags.Child, parent);
} else {
Logger.warn(`The entity ${this.name} is not in the hierarchy`);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: invalid if/else/else chain — blocks all CI builds.

The block goes if (this._isRoot) { … } else { … } else { … }, which is not valid TypeScript and breaks every CI job (rollup/swc, ESLint, Biome) at line 221. Per the PR description, the second branch should be else if (this._parent) so orphan entities fall through to the Logger.warn path.

🛠️ Proposed fix
   set siblingIndex(value: number) {
     if (this._isRoot) {
       this._setSiblingIndex(this._scene._rootEntities, value);
-    } else {
+    } else if (this._parent) {
       const parent = this._parent;
       this._setSiblingIndex(parent._children, value);
       parent._dispatchModify(EntityModifyFlags.Child, parent);
     } else {
       Logger.warn(`The entity ${this.name} is not in the hierarchy`);
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set siblingIndex(value: number) {
if (this._siblingIndex === -1) {
throw `The entity ${this.name} is not in the hierarchy`;
}
if (this._isRoot) {
this._setSiblingIndex(this._scene._rootEntities, value);
} else {
const parent = this._parent;
this._setSiblingIndex(parent._children, value);
parent._dispatchModify(EntityModifyFlags.Child, parent);
} else {
Logger.warn(`The entity ${this.name} is not in the hierarchy`);
}
}
set siblingIndex(value: number) {
if (this._isRoot) {
this._setSiblingIndex(this._scene._rootEntities, value);
} else if (this._parent) {
const parent = this._parent;
this._setSiblingIndex(parent._children, value);
parent._dispatchModify(EntityModifyFlags.Child, parent);
} else {
Logger.warn(`The entity ${this.name} is not in the hierarchy`);
}
}
🧰 Tools
🪛 Biome (2.4.14)

[error] 221-221: Expected a statement but instead found 'else'.

(parse)

🪛 GitHub Actions: CI / 0_e2e (22.x, 2_4).txt

[error] 221-221: Rollup build failed via (plugin swc) with syntax error: "Expression expected" at line 221 (} else {).

🪛 GitHub Actions: CI / 1_build (22.x, ubuntu-latest).txt

[error] 221-221: Build failed during Rollup (plugin swc) with syntax error: "Expression expected" at "} else {".

🪛 GitHub Actions: CI / 2_e2e (22.x, 3_4).txt

[error] 221-223: Build failed in rollup (plugin swc): Syntax Error: "Expression expected" at the closing brace before else.

🪛 GitHub Actions: CI / 3_e2e (22.x, 1_4).txt

[error] 221-221: Build failed in rollup (plugin swc): Syntax Error: 'Expression expected' near '}' / start of 'else' block. See swc error at /Users/runner/work/engine/engine/packages/core/src/Entity.ts:221:1.

🪛 GitHub Actions: CI / 5_lint.txt

[error] 221-221: ESLint parsing error: Declaration or statement expected (Parsing error: Declaration or statement expected).

🪛 GitHub Actions: CI / 6_codecov.txt

[error] 221-221: Build failed during Rollup (plugin swc). Syntax error: Expression expected at line 221 near 'else'.

🪛 GitHub Actions: CI / 7_build (22.x, macos-latest).txt

[error] 221-221: rollup (plugin swc) build failed with Syntax Error: "Expression expected" at } else {

🪛 GitHub Actions: CI / 8_e2e (22.x, 4_4).txt

[error] 221-221: Build failed in rollup (plugin swc): Syntax Error — "Expression expected" at line 221 near "} else {".

🪛 GitHub Actions: CI / build (22.x, macos-latest)

[error] 221-221: Build failed in rollup (plugin swc): Syntax Error: "Expression expected" at line 221 near "} else {".

🪛 GitHub Actions: CI / build (22.x, ubuntu-latest)

[error] 221-221: Rollup (plugin swc) failed with syntax error: "Expression expected" at Entity.ts:221:1 near } else {.

🪛 GitHub Actions: CI / codecov

[error] 221-221: Build failed during rollup compilation with (plugin swc) syntax error: "Expression expected" at the } else { line. Error reported as: "(plugin swc) Error: Expression expected".

🪛 GitHub Actions: CI / e2e (22.x, 1_4)

[error] 221-224: Build failed during Rollup (plugin swc): "Expression expected" Syntax Error at "/Users/runner/work/engine/engine/packages/core/src/Entity.ts:221:1" near "+ } else {"

🪛 GitHub Actions: CI / e2e (22.x, 2_4)

[error] 221-221: Build failed during rollup with plugin swc: Syntax Error — "Expression expected" at line 221 near "} else {".

🪛 GitHub Actions: CI / e2e (22.x, 3_4)

[error] 221-221: Build failed in rollup (plugin swc): 'Expression expected' syntax error at Entity.ts line 221 near '} else {'.

🪛 GitHub Actions: CI / e2e (22.x, 4_4)

[error] 221-223: Build failed during Rollup (plugin swc). swc syntax error in /Users/runner/work/engine/engine/packages/core/src/Entity.ts:221:1: "Expression expected" near "} else {".

🪛 GitHub Actions: CI / lint

[error] 221-221: ESLint parsing error: Declaration or statement expected

🤖 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 214 - 224, The setter siblingIndex
has an invalid if/else/else chain; update the second branch to be an else if
that checks for this._parent so orphans fall through to the Logger.warn path:
keep the this._isRoot branch that calls
this._setSiblingIndex(this._scene._rootEntities, value), change the middle
branch to else if (this._parent) which calls
this._setSiblingIndex(parent._children, value) and
parent._dispatchModify(EntityModifyFlags.Child, parent), and let the final else
call Logger.warn(`The entity ${this.name} is not in the hierarchy`).

Comment on lines +332 to +353
const parentModifyCount = [0, 0, 0];
const childModifyCount = [0, 0, 0];
const child2ModifyCount = [0, 0, 0];
// @ts-ignore
parent._registerModifyListener((flag: EntityModifyFlags) => ++parentModifyCount[flag]);
// @ts-ignore
child._registerModifyListener((flag: EntityModifyFlags) => ++childModifyCount[flag]);
// @ts-ignore
child2._registerModifyListener((flag: EntityModifyFlags) => ++child2ModifyCount[flag]);

parent.clearChildren();
expect(parent.children.length).eq(0);

// Parent should receive a single `Child` modify event for the whole clear so
// listeners (e.g. UICanvas) can invalidate their cached state.
expect(parentModifyCount[EntityModifyFlags.Child]).eq(1);
// Each detached child should receive a `Parent` modify event.
expect(childModifyCount[EntityModifyFlags.Parent]).eq(1);
expect(child2ModifyCount[EntityModifyFlags.Parent]).eq(1);
// Sibling index must be reset so the entity is treated as lonely afterwards.
expect(child.siblingIndex).eq(-1);
expect(child2.siblingIndex).eq(-1);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 💤 Low value

Heads up: parentModifyCount[EntityModifyFlags.Child]).eq(1) only holds if Entity.clearChildren() dispatches once for the whole operation.

The current implementation in packages/core/src/Entity.ts dispatches the Child event inside the per-child loop, so this assertion will see 2. See the corresponding comment on Entity.clearChildren(); aligning the two is required before this test will pass.

🤖 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/Entity.test.ts` around lines 332 - 353, Test expects
Entity.clearChildren() to dispatch a single EntityModifyFlags.Child event for
the whole operation, but current implementation emits Child inside the per-child
loop; update the Entity.clearChildren() method to accumulate removals and call
the modify/dispatch for EntityModifyFlags.Child exactly once after all children
are detached (while still emitting the Parent modify event per detached child
and resetting siblingIndex to -1 for each child); locate the dispatch call(s) in
Entity.clearChildren() and move/condense them so only one Child event is emitted
for the overall clear.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

GuoLei1990

This comment was marked as outdated.

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.

已关闭问题清单

问题 状态
clearChildren 缺少 child._siblingIndex = -1 重置 ✅ 已修复
clearChildren 缺少 child._setParentChange() ✅ 已修复
SceneManager.loadScene destroy 遍历时修改正在遍历的数组 ✅ 已修复(getLoopArray()

总结

方向正确,三项历史 bug 均已处理。但仍有两个问题未修复。


问题

[P0] Entity.ts:220set siblingIndex 中存在 if/else/else 语法错误,无法通过 TypeScript 编译

当前代码结构:

if (this._isRoot) {
  this._setSiblingIndex(this._scene._rootEntities, value);
} else {
  const parent = this._parent;
  this._setSiblingIndex(parent._children, value);
  parent._dispatchModify(EntityModifyFlags.Child, parent);
} else {                            // ← 第二个 else,非法
  Logger.warn(`The entity ${this.name} is not in the hierarchy`);
}

这是编译级错误,会导致 CI 全线失败。正确结构:

if (this._isRoot) {
  this._setSiblingIndex(this._scene._rootEntities, value);
} else if (this._parent) {
  const parent = this._parent;
  this._setSiblingIndex(parent._children, value);
  parent._dispatchModify(EntityModifyFlags.Child, parent);
} else {
  Logger.warn(`The entity ${this.name} is not in the hierarchy`);
}

[P0] Entity.ts:409clearChildren 循环内每次 dispatch Child 事件,测试断言必然 fail

当前实现:

for (let i = children.length - 1; i >= 0; i--) {
  ...
  this._dispatchModify(EntityModifyFlags.Child, this); // 每个子节点各触发一次
  ...
}

有 2 个子节点时,parentModifyCount[EntityModifyFlags.Child] 值为 2,但测试在第 350 行断言:

expect(parentModifyCount[EntityModifyFlags.Child]).eq(1);

clearChildren 是批量操作,父节点只应收到一次 Child 事件。实现与测试自相矛盾,两者必有一个是错的。

测试语义是正确的,应当修改实现:将 _dispatchModify 移到循环之前(不是之后),让订阅者在所有子节点被移除前收到一次通知,之后在循环内执行清理:

clearChildren(): void {
  const children = this._children;
  if (children.length === 0) return;
  // Notify listeners once before detaching any child, so subscribers (e.g. UICanvas)
  // can react while all children are still accessible.
  this._dispatchModify(EntityModifyFlags.Child, this);
  for (let i = children.length - 1; i >= 0; i--) {
    const child = children[i];
    child._parent = null;
    child._siblingIndex = -1;
    // ... activeChangeFlag logic ...
    child._processInActive(activeChangeFlag);
    Entity._traverseSetOwnerScene(child, null);
    child._setParentChange();
  }
  children.length = 0;
}

这同时解决了注释里提到的时序问题(dispatch 在 _processInActive 之前),又保证只触发一次。

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.

总结

修复 Entity.clearChildren() 中两个 bug:

  1. 子节点 _siblingIndex 未重置:detach 后子节点仍持有旧 siblingIndex(>= 0),setSiblingIndex 的检测逻辑会认为它仍在层级中,引发错误行为
  2. _setParentChange() 未调用:子节点 detach 后未通知订阅者父节点变更,订阅了 Parent modify 事件的系统(如 Transform dirty)无法感知

同时修复 setSiblingIndex setter 中死代码 if (this._siblingIndex === -1) throwelse 错位导致的逻辑断裂,改为 Logger.warn(非 throw)语义也更合理——孤立节点设置 siblingIndex 是用户误操作,不是程序错误。

测试通过公开 API 触发,断言公开输出(parent.children.length、子节点 siblingIndex、modify 事件计数),正确的链路测试。

问题

[P2] clearChildren_dispatchModify 在循环内被重复调用

当前代码在每个子节点的循环体内都调用 this._dispatchModify(EntityModifyFlags.Child, this),父节点会被通知 N 次(有几个子节点就通知几次)。如果订阅者(如 UICanvas)在每次通知时做复杂操作(如 layout invalidation),会有 O(N) 重复开销。

Unity 在 ClearChildren 等批量操作中只在完成后发送一次 dirty 通知。

测试断言 parentModifyCount[EntityModifyFlags.Child] === 1,但实际实现在 N 个子节点时发送 N 次,测试会失败(除非测试数据只有 1 个子节点时巧合通过)。——验证:测试用了两个子节点(child + child2),若当前实现发 2 次,parentModifyCount[Child] === 1 断言会 fail。建议将 _dispatchModify 移到循环外,在 children.length = 0 后统一发一次。

// 修复:移出循环
for (let i = ...) {
  child._siblingIndex = -1;
  // 不在这里 dispatch
  // ...
  child._setParentChange();
}
children.length = 0;
this._dispatchModify(EntityModifyFlags.Child, this); // 一次性通知

LGTM(P2 处理后合入)。

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