Skip to content

[Physics] Colliders create the native actor & register the transform listener in the constructor instead of on enter-scene (stale pose under clone/instantiate) #3021

Description

@cptbtptpbcptdtptp

Summary

All colliders create their native physics actor — and read transform.worldPosition / register the transform change-listener — in the component constructor rather than on enter-scene. During entity.clone() (and prefab instantiate / deserialize) the cloned entity's transform is only populated after its components are constructed, so the native actor is seeded with a stale (0,0,0) pose. Correctness then depends on a later transform-change dispatch firing — which the bulk-write clone path (Transform._cloneTo, which nulls _onValueChanged then copyFroms) bypasses.

Where

  • packages/core/src/physics/DynamicCollider.ts — constructor reads transform.worldPosition / worldRotationQuaternion and creates the native actor.
  • packages/core/src/physics/StaticCollider.ts / CharacterController.ts — same constructor-time pattern.
  • packages/core/src/physics/Collider.ts — registers the world-change listener in the constructor: this._updateFlag = entity.registerWorldChangeFlag().

Current workaround (symptom patch)

Transform._cloneTo was made to eagerly re-dispatch the world dirty flags — target._worldAssociatedChange(TransformModifyFlags.WmWpWeWqWsWus) — so the collider's listener re-fires after clone (the "fix Transform clone dirty flag" change on the physics branch). This patches the symptom on the Transform side; the root cause is the collider lifecycle.

Why that's the wrong layer

Eagerly dispatching transform changes during clone is not what other engines do — they create native/physics state after the transform is finalized, on enter-scene:

  • Unreal: physics state is created in OnCreatePhysicsState, which runs during registration after OnRegisterUpdateComponentToWorld finalizes the transform. PIE duplication is PostDuplicate → the normal register flow. No dispatch-on-duplicate.
  • Unity: Instantiate copies local TRS, world matrix is lazily recomputed; the Rigidbody body is created in Awake/OnEnable, after the transform is set.
  • Cocos: Node._onBatchCreated (the clone path) only sets dirty bits (_transformFlags |= TRS); it does not emit TRANSFORM_CHANGED. Physics syncs in onEnable.

The shared rule: keep transform dirty bits unconditionally (lazy recompute), but only register listeners / create native state on enter-scene, after the transform is finalized. Then clone / deserialize "just work" with no eager dispatch.

Proposed fix

For every collider, move out of the constructor into the scene lifecycle:

  1. native actor creation + worldPosition read → _onEnableInScene
  2. _updateFlag = registerWorldChangeFlag()_onEnableInScene; release in _onDisableInScene
  3. property setters / addShape tolerate a not-yet-created native (write the JS field; do a one-shot _syncNative() on enable — JS fields are the source of truth)

Then the Transform._cloneTo_worldAssociatedChange workaround can be removed.

Prototype (StaticCollider only)

Moving StaticCollider's native creation to _onEnableInScene (reading the final transform) + null-native guards in the base Collider: 62 physics tests pass (PhysicsMaterial, MeshColliderShape incl. cloned-mesh-shape, Collider, Collision) with no Transform dispatch involved. DynamicCollider / CharacterController need the same treatment — larger, because of the many property setters that must guard a missing native and sync on enable.

Context

Surfaced while re-architecting the clone system (#3018). As clone / prefab-instantiate becomes a first-class, relied-upon path after that lands, entities with colliders will hit the stale-pose more visibly, and the Transform-side workaround will be the thing keeping it afloat. Filing so the root-cause lifecycle fix is tracked separately.

Regression tests (belong here, not in #3018)

These two Transform._cloneTo tests encode the stale-world-cache / missed-listener-dispatch scenario. They were written during #3018 but moved here because they document this issue, not the clone re-architecture. They currently fail on a base without the Transform workaround. (Imports: Entity, TransformModifyFlags from @galacean/engine-core; engine from the suite's beforeAll.)

it("clone — _cloneTo re-dirties the world cache cleared by an earlier component's ctor read", () => {
  const source = new Entity(engine, "source");
  source.transform.setPosition(1, 2, 3);

  const target = new Entity(engine, "target");
  expect(target.transform.worldPosition.x).to.equal(0);
  // @ts-ignore — the getter above cleared the WorldPosition dirty flag
  expect(target.transform._dirtyFlag & TransformModifyFlags.WorldPosition).to.equal(0);

  // @ts-ignore — internal clone hook
  source.transform._cloneTo(target.transform);

  const afterClone = target.transform.worldPosition;
  expect(afterClone.x).to.equal(1);
  expect(afterClone.y).to.equal(2);
  expect(afterClone.z).to.equal(3);
});

it("clone — _cloneTo dispatches the world-flag change to registered listeners", () => {
  const source = new Entity(engine, "source");
  source.transform.setPosition(7, 8, 9);

  const target = new Entity(engine, "target");
  const updateFlag = target.registerWorldChangeFlag();
  target.transform.worldPosition; // clears + caches, mirrors a component ctor read
  updateFlag.flag = false;

  // @ts-ignore — internal clone hook
  source.transform._cloneTo(target.transform);

  expect(updateFlag.flag).to.equal(true);
});

Note: the two tests above assert the Transform-side dispatch (i.e. the _worldAssociatedChange workaround). The root-cause fix (collider native creation → enter-scene) should additionally be guarded by a collider-level test: clone an entity whose collider sits at a non-origin world position, add the clone to the scene, then assert the native actor's world pose matches the entity (not the constructor-default origin).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions