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.

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 重置 ✅ 已修复
clearChildren 缺少 child._setParentChange() ✅ 已修复
SceneManager.loadScene destroy 遍历时修改正在遍历的数组 ✅ 已修复(getLoopArray())
siblingIndex setter 无父节点时应 warn 而非 throw ✅ 已修复(Logger.warn)

总结

两处 Entity hierarchy bug 修复方向均正确,clearChildren 的状态清理完整,SceneManager 并发迭代问题已修复。但 P0 语法错误和 P1 dispatch 过度仍存在,需修复后再合入。

问题

[P0] Entity.ts:215siblingIndex setter 存在 if/else/else 语法错误,TypeScript 无法编译

当前 HEAD 代码(已通过读取分支确认):

set siblingIndex(value: number) {
  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`);
  }
}

结构应为三路分支:_isRoot / _parent != null / 孤立。修复:

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`);
  }
}

[P1] Entity.ts:401clearChildren 在循环内每次调用 _dispatchModify(EntityModifyFlags.Child, this),触发 N 次

removeChild 只触发一次 EntityModifyFlags.ChildclearChildren 改后在 N 个子节点的循环内触发 N 次,UICanvas 等订阅者执行 N 次无效刷新。测试也断言 parentModifyCount[EntityModifyFlags.Child] === 1,但当前实现会触发 N 次,测试自身也会失败(除非 N=1)。

建议将 _dispatchModify 移出循环,在 children.length = 0 之后调用一次:

clearChildren(): void {
  const children = this._children;
  for (let i = children.length - 1; i >= 0; i--) {
    const child = children[i];
    child._parent = null;
    child._siblingIndex = -1;

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

    Entity._traverseSetOwnerScene(child, null);
    child._setParentChange();
  }
  children.length = 0;
  this._dispatchModify(EntityModifyFlags.Child, this);  // ← 移到循环外,触发一次
}

注意:_dispatchModify 需要在 children.length = 0 之后调用,而不是之前——让订阅者看到已清空的状态。

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 重置 ✅ 已修复
clearChildren 缺少 child._setParentChange() ✅ 已修复
SceneManager.loadScene destroy 遍历时修改正在遍历的数组 ✅ 已修复(getLoopArray())
siblingIndex setter 无父节点时应 warn 而非 throw ✅ 意图正确,但实现有语法错误

问题

[P0] Entity.ts:213siblingIndex setter 存在 if/else/else 语法错误,TypeScript 无法编译

当前 diff 生成的代码结构:

set siblingIndex(value: number) {
  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,TypeScript 语法错误
    Logger.warn(`The entity ${this.name} is not in the hierarchy`);
  }
}

这是一个有三路分支的逻辑,应该使用 if / else if / else

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

此错误已在第 3 轮 review 指出,多轮未修复。

[P1] Entity.ts:404clearChildren 在循环内每次调用 _dispatchModify(EntityModifyFlags.Child, this)

当前循环体内 dispatch N 次,但:

  1. 测试断言 parentModifyCount[EntityModifyFlags.Child] === 1,实际会得到 N(2 个 child 就是 2)—— 测试自身会失败
  2. removeChild 的语义是每次 dispatch 1 次,clearChildren 整体只需 dispatch 1 次

建议将 _dispatchModify 移出循环,在 children.length = 0 之后调用:

clearChildren(): void {
  const children = this._children;
  for (let i = children.length - 1; i >= 0; i--) {
    const child = children[i];
    child._parent = null;
    child._siblingIndex = -1;

    let activeChangeFlag = ActiveChangeFlag.None;
    child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy);
    child._isActive && (activeChangeFlag |= ActiveChangeFlag.Self);
    activeChangeFlag && child._processInActive(activeChangeFlag);

    Entity._traverseSetOwnerScene(child, null);
    child._setParentChange();
  }
  children.length = 0;
  this._dispatchModify(EntityModifyFlags.Child, this);  // ← 移到循环外,整体 dispatch 1 次
}

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.

增量审查(2026-05-15)

已关闭问题清单

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

以下两个问题确认仍未修复


[P0] Entity.ts:213siblingIndex setter 存在 else/else 语法错误,TypeScript 无法编译

diff 生成的代码结构:

set siblingIndex(value: number) {
  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`);
  }
}

三路分支应使用 if / else if / else

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

此问题已在第三轮 review 指出,当前 PR diff 仍携带错误结构。


[P1] Entity.ts:404clearChildren 在循环内每次都调用 _dispatchModify(EntityModifyFlags.Child, this)

当前实现在循环体内 dispatch N 次(每个 child 各一次),但:

  1. 测试断言 parentModifyCount[EntityModifyFlags.Child]).eq(1),实际结果等于子节点数量——测试自身会失败
  2. clearChildren 整体只需一次 Child notify,应移出循环

建议:将 _dispatchModify 移到循环之外,在 children.length = 0 之后调用:

clearChildren(): void {
  const children = this._children;
  for (let i = children.length - 1; i >= 0; i--) {
    const child = children[i];
    child._parent = null;
    child._siblingIndex = -1;

    let activeChangeFlag = ActiveChangeFlag.None;
    child._isActiveInHierarchy && (activeChangeFlag |= ActiveChangeFlag.Hierarchy);
    child._isActive && (activeChangeFlag |= ActiveChangeFlag.Self);
    activeChangeFlag && child._processInActive(activeChangeFlag);

    Entity._traverseSetOwnerScene(child, null);
    child._setParentChange();
  }
  children.length = 0;
  this._dispatchModify(EntityModifyFlags.Child, this); // 一次,在循环外
}

以上 P0/P1 修复后可合入。

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.

增量审查(2026-05-15)

已关闭问题清单

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

以下两个问题确认仍未修复


[P0] Entity.ts:213siblingIndex setter 存在 if/else/else 语法错误,TypeScript 无法编译

diff 生成的代码结构为:

set siblingIndex(value: number) {
  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`);
  }
}

这是无效的 TypeScript 语法,会导致 tsc、rollup 等所有构建工具报错,CI 全线失败。

正确写法:

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

[P1] clearChildren_dispatchModify 在循环内被调用 n 次,但测试断言只期望 1 次

当前循环结构:

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

clearChildren 是一个批量操作,订阅者(如 UICanvas)在收到 Child 事件后会触发完整的重新计算。对 n 个子节点触发 n 次,会导致 O(n²) 的更新开销。

测试文件 Entity.test.ts:351 中:

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

该断言当前因为循环内调用了 n 次而会失败(n=2 时,parentModifyCount[EntityModifyFlags.Child] 为 2)。

正确做法:将 dispatch 移出循环,在所有子节点处理完毕后调用一次:

for (let i = children.length - 1; i >= 0; i--) {
  const child = children[i];
  child._parent = null;
  child._siblingIndex = -1;
  // ... processInActive, traverseSetOwnerScene, _setParentChange
}
children.length = 0;
this._dispatchModify(EntityModifyFlags.Child, this);  // ← 移出循环

请修复以上两个问题后再合入。

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.

增量审查(2026-05-15)

已关闭问题清单

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

以下两个问题确认仍未修复


[P0] Entity.ts:215siblingIndex setter 存在 if/else/else 语法错误,TypeScript 无法编译

当前 diff 生成的代码结构:

set siblingIndex(value: number) {
  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 挂在同一个 if 上,TS 语法错误
    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) {
    this._setSiblingIndex(this._parent._children, value);
    this._parent._dispatchModify(EntityModifyFlags.Child, this._parent);
  } else {
    Logger.warn(`The entity ${this.name} is not in the hierarchy`);
  }
}

[P1] clearChildren 在循环内每次都调用 _dispatchModify(EntityModifyFlags.Child, this),触发 N 次而非 1 次

当前代码在 for 循环体内调用 this._dispatchModify(EntityModifyFlags.Child, this),有 N 个子节点就触发 N 次 Child modify 事件。测试断言 parentModifyCount[EntityModifyFlags.Child].eq(1) 在只有 2 个子节点时偶然通过是不可能的——实际上会是 2,测试本身也会失败。

正确做法是把 _dispatchModify 移到循环,整个 clearChildren 完成后发一次:

_clearChildren(): void {
  const children = this._children;
  for (let i = children.length - 1; i >= 0; i--) {
    const child = children[i];
    child._parent = null;
    child._siblingIndex = -1;
    // ... processInActive, traverseSetOwnerScene, _setParentChange ...
  }
  children.length = 0;
  this._dispatchModify(EntityModifyFlags.Child, this);  // ← 一次,在循环外
}

请修复以上两个问题后重新提交。

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