Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
79e9105
refactor(clone): add @defaultCloneMode and type-driven clone for cont…
Jun 17, 2026
a22e83c
refactor(clone): rewrite clone system — opt-out + default Assignment …
Jun 17, 2026
afce33d
refactor(clone): mark value-semantic classes with @defaultCloneMode(D…
Jun 17, 2026
94e6a03
fix(clone): remap nested Entity/Component refs via two-pass clone
Jun 22, 2026
a0e5fa9
refactor(clone): three-tier mode resolution + container default deep
Jun 22, 2026
297508f
refactor(clone): drop 171 field clone-mode decorators, rely on type d…
Jun 22, 2026
c5d6257
Merge remote-tracking branch 'upstream/dev/2.0' into fix/clone-opt-ou…
Jun 23, 2026
00498a1
ci: shard codecov unit tests (4 shards) to avoid browser crash on ful…
Jun 23, 2026
1561b72
docs(clone): drop CLONE_OPT_IN_AUDIT.md; design moved to PR description
Jun 23, 2026
d196914
ci: split codecov into 8 shards (vitest browser WebGL resource ceiling)
Jun 23, 2026
3cdd18c
test(clone): regression for texture refCount balance on clone/destroy
Jun 23, 2026
b8e754c
ci: revert codecov sharding experiment (let GitHub run the original f…
Jun 23, 2026
2139275
refactor(clone): decouple ref counting from the clone gate and unify …
Jul 2, 2026
13f3d95
Merge branch 'dev/2.0' into fix/clone-opt-out-assignment
Jul 2, 2026
9c5469c
refactor(clone): slot-ownership ref counting — gate acquires, owner r…
Jul 3, 2026
c2790dd
test(clone): refCount lifecycle suites + fix particle MeshShape mesh …
Jul 3, 2026
60135e9
refactor(clone)!: remove the deprecated shallowClone decorator
Jul 3, 2026
2a4309e
style(clone): order CloneManager members by visibility
Jul 3, 2026
88625db
refactor(clone): exempt user scripts from gate ref counting
Jul 3, 2026
2239fbd
refactor(clone): one uniform slot contract — no script special-casing
Jul 3, 2026
c7df2af
refactor(clone): move slot-ownership acquisition out of the gate
Jul 3, 2026
84e1b76
docs(clone): clarify the @deepClone-on-Remap branch is error recovery
Jul 3, 2026
3c26c2a
docs(clone): condense the CloneManager class comment
Jul 3, 2026
5b8da5a
test(clone): pin aliasing topology — shared instance clones into one …
Jul 3, 2026
e5894ae
refactor(clone): drop _cloneTo logic the type-driven gate already covers
Jul 3, 2026
8ef2a64
fix(mesh): type _onSkinUpdated's value as the union it actually carries
Jul 3, 2026
158fbdc
refactor(math): add a MathValue base class carrying the family clone …
Jul 3, 2026
1be50b0
Revert "refactor(math): add a MathValue base class carrying the famil…
Jul 3, 2026
5e3738d
test(clone): guard math value-type registration completeness
Jul 3, 2026
7dba7c1
feat(math): implement IClone/ICopy on Ray and register it Deep
Jul 3, 2026
de460e8
fix(clone): treat null-prototype objects as data containers
Jul 3, 2026
6c4cbb7
fix(clone): @deepClone on a registered asset recovers to sharing
Jul 3, 2026
56fb595
docs(clone): tighten comments in CloneManager and ComponentCloner
Jul 3, 2026
6a8688e
feat(clone): register runtime containers @defaultCloneMode(Ignore)
Jul 3, 2026
a3510d4
refactor(clone): drop @ignoreClone on runtime-container typed fields
Jul 3, 2026
887bfcc
docs(clone): define Deep as structure-fresh, member-semantic cloning
Jul 3, 2026
83e8328
docs(clone): correct stale or vacuous comments in gate and cloner
Jul 3, 2026
bdcc635
refactor(ui): centralize transition state ref counting in the base class
Jul 3, 2026
81c7991
test(math): cover Ray clone and copyFrom
Jul 3, 2026
e696681
fix(clone): release an owned counted preset displaced by an uncounted…
Jul 3, 2026
b0064ab
fix(clone): register typed-array clones in the identity map and harde…
Jul 3, 2026
d095812
fix(clone): route copyFrom dispatch after containers and close review…
Jul 3, 2026
6d0c7fe
test(clone): cover the rewritten Skin, PostProcess and TrailRenderer …
Jul 3, 2026
78ec199
docs(clone): align clone guides with the type-driven mechanism
Jul 3, 2026
9fa3cc2
refactor(clone): settle slot ownership unconditionally and fold dupli…
Jul 4, 2026
23f0459
test(clone): rename the suite after CloneManager and fix stale attrib…
Jul 4, 2026
2a91917
perf(clone): iterate Map/Set with for-of to avoid per-clone closure a…
Jul 4, 2026
c13dd08
docs(clone): drop a change-narrating comment on the Map branch
Jul 4, 2026
6d724c7
docs(clone): drop tense-relative wording from a lifecycle test comment
Jul 4, 2026
828008f
fix(particle): allow bare construction of ParticleCompositeGradient
Jul 4, 2026
16d95d6
test(clone): enforce the bare-construction contract for Deep-register…
Jul 4, 2026
f58d380
feat(clone): name the bare-construction contract when a deep clone ca…
Jul 4, 2026
843ab78
test(clone): pin the host-bound-in-container rejection as intended se…
Jul 4, 2026
9db6c66
test(clone): pin both host-bound-in-container behaviors by component …
Jul 4, 2026
f8ad0d7
fix(shader): cascade texture-array entries through the ref-count cont…
Jul 4, 2026
7ba6d16
Merge remote-tracking branch 'origin/dev/2.0' into fix/clone-opt-out-…
Jul 10, 2026
7027eec
test(clone): pin upstream orbital fields deep-cloning via the type de…
Jul 10, 2026
ec48059
test(clone): fix orbital pin to use the public centerOffset accessor
Jul 10, 2026
cd3bad1
docs(clone): document defaultCloneMode registration for custom types
Jul 10, 2026
033f938
refactor(clone): fold Deep registration into a public DataObject base…
Jul 17, 2026
aea87ef
refactor(clone): replace the marker decorator with an instanceof defa…
Jul 17, 2026
36f7446
perf(clone): let DataObject.clone() skip mode resolution it already k…
Jul 17, 2026
bcf668f
refactor(clone): collapse mode-then-switch into decide-and-execute
Jul 17, 2026
5d27b6f
test(clone): cover @ignoreClone on a function field (previously untes…
Jul 17, 2026
f0262a4
refactor(clone): replace the field-mode registry with prototype-chain…
Jul 18, 2026
cf90bd6
refactor(clone): unify the two ad hoc _isDeepCloneType stamping sites
Jul 18, 2026
60e5cd3
fix(clone): fix three bugs in the DataObject template-method rewrite
Jul 18, 2026
6f72fb4
refactor(clone): shrink DataObject to a bare instanceof marker class
Jul 18, 2026
f8d2ff1
refactor(clone): split _deepClone into a dispatcher plus per-shape me…
Jul 18, 2026
13f1b11
refactor(clone): dispatch directly by type, dropping _isContainer + _…
Jul 18, 2026
86f4bef
refactor(clone): inline per-shape clone; fix @deepClone on a default-…
Jul 18, 2026
ec3e90b
fix(clone): throw on @deepClone of an engine-bound type instead of fa…
Jul 18, 2026
c547a03
refactor(clone): split execution into CloneUtil, drop the injection s…
Jul 18, 2026
9ee44e3
refactor(clone): drop the unused CloneMode.Remap member
Jul 18, 2026
7e46e9f
refactor(clone): fold the deep-clone assertion back into the type bra…
Jul 18, 2026
7248f0a
refactor(clone): name CloneUtil parameters source / preset consistently
Jul 18, 2026
4f86f9f
refactor(clone): inline the remap-type check into Signal, drop the de…
Jul 18, 2026
3a73a7c
refactor(clone): rename _cloneByType to _cloneByDefault
Jul 18, 2026
55f0009
fix(core): type UpdateFlagManager dispatch param as unknown, not object
Jul 18, 2026
62b9a3b
fix(clone): dedup per producing branch, not before the type dispatch
Jul 18, 2026
5509233
refactor(clone): flatten _cloneByDefault into one dispatch chain
Jul 18, 2026
09fdbd5
docs(clone): strip restating JSDoc from internal methods
Jul 18, 2026
f6ee729
style(clone): use the multi-line @internal block on methods
Jul 18, 2026
cdb0d1b
refactor(clone): prefix deepCloneObject with an underscore
Jul 18, 2026
6d20862
fix(clone): check the function branch before the non-object guard
Jul 18, 2026
7a1be3d
refactor(clone): replace the fieldMode switch with a guard and a deri…
Jul 18, 2026
08bab73
test(clone): reach the field walk through the public API, unexport Cl…
Jul 18, 2026
e663cb2
refactor(particle): let the gate clone CustomDataModule's streams
Jul 18, 2026
44756d8
test(particle): assert cloned custom-data streams field by field
Jul 18, 2026
aa8f4e8
docs(clone): state constraints instead of narrating the change
Jul 18, 2026
5ffb29d
fix(core): drop @internal from DataObject so its declaration survives
Jul 19, 2026
68616fc
docs(clone): drop comments restating gate behavior at call sites
Jul 19, 2026
1d554f6
docs(clone): drop the gate explanation from SubEmitter's back-link
Jul 19, 2026
0494dca
docs(clone): drop the _createCloneEntity block comment
Jul 19, 2026
030825f
docs(clone): drop remaining comments that restate the code
Jul 19, 2026
702d1a7
perf(clone): walk own keys instead of for...in
Jul 19, 2026
636d19d
docs(clone): cut comments down to the non-derivable constraints
Jul 19, 2026
a97b5b4
docs(clone): drop the remaining inline comments
Jul 19, 2026
a1eb05c
refactor(shader): make _addTexturesReferCount an instance method
Jul 19, 2026
9be79cf
docs(clone): drop narrative comments from Signal and Transition
Jul 19, 2026
36472b3
fix(clone): let @deepClone override the plain-container default
Jul 19, 2026
b0f8226
refactor(clone): fold the two cloning branches into one
Jul 19, 2026
3d06a6d
refactor(clone): extract the construct-and-register step
Jul 19, 2026
7295735
refactor(clone): rename _deepConstructObject to _createCloneTarget
Jul 19, 2026
dbb7624
docs(clone): document the deep-default opt-ins, fix the release guidance
Jul 19, 2026
983fe59
fix(core): stop re-exporting the stripped CloneManager class
Jul 19, 2026
a4dc14b
docs(clone): close the review-found gaps in the rules
Jul 19, 2026
f37945f
test(clone): pin the review-found contract gaps
Jul 19, 2026
64410a6
feat(clone): @deepClone deep-clones the whole subtree
Jul 19, 2026
8a95423
style(docs): run prettier over the clone pages
Jul 19, 2026
118a7a5
fix(clone): throw on @deepClone decorating a function
Jul 19, 2026
c61e068
refactor(clone): rename the assert and trim comments
Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 100 additions & 36 deletions docs/en/core/clone.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,96 +8,160 @@ label: Core
Node cloning is a common runtime feature, and node cloning also includes cloning its bound components. For example, during the initialization phase, dynamically create a certain number of identical entities based on configuration, and then place them in different positions in the scene according to logical rules. Here, the details of script cloning will be explained in detail.

## Entity Cloning

It's very simple, just call the entity's [clone()](/apis/design/#IClone-clone) method to complete the cloning of the entity and its attached components.

```typescript
const cloneEntity = entity.clone();
```

## Script Cloning
Scripts are essentially components, so when we call the entity's [clone()](/apis/design/#IClone-clone) function, the engine will not only clone the built-in components but also clone custom scripts. The cloning rules for built-in components have been customized by the official team, and similarly, we have also opened up the cloning capabilities and rules for scripts to developers. The default cloning method for script fields is shallow copy. For example, if we modify the field values of the script and then clone it, the cloned script will retain the modified values without any additional coding. Below is an example of custom script cloning:

Scripts are essentially components, so when we call the entity's [clone()](/apis/design/#IClone-clone) function, the engine will not only clone the built-in components but also clone custom scripts. The cloning rules for built-in components have been customized by the official team, and similarly, we have also opened up the cloning capabilities and rules for scripts to developers. Script fields are cloned automatically, and how each field value is cloned is resolved by the value's type (see the default rules below). For example, if we modify the field values of the script and then clone it, the cloned script will retain the modified values without any additional coding. Below is an example of custom script cloning:

```typescript
// define a custom script
class CustomScript extends Script{
class CustomScript extends Script {
/** boolean type.*/
a:boolean = false;
a: boolean = false;

/** number type.*/
b:number = 1;
b: number = 1;

/** class type.*/
c:Vector3 = new Vector3(0,0,0);
c: Vector3 = new Vector3(0, 0, 0);
}

// Init entity and script
const entity = engine.createEntity();
const script = entity.addComponent(CustomScript);
script.a = true;
script.b = 2;
script.c.set(1,1,1);
script.c.set(1, 1, 1);

// Clone logic
const cloneEntity = entity.clone();
const cloneScript = cloneEntity.getComponent(CustomScript);
console.log(cloneScript.a); // output is true.
console.log(cloneScript.b); // output is 2.
console.log(cloneScript.c); // output is (1,1,1).
console.log(cloneScript.c); // output is (1,1,1), and it is an independent copy — Vector3 is a math value type and is deep cloned by default.
```

### Default Cloning Rules

When a field has no clone decorator, the engine resolves how to clone its value by the value's type:

| Value Type | Default Cloning Behavior |
| :-- | :-- |
| Primitive types (`number`, `string`, `boolean`, ...) | Copy the value. |
| Assets (`Texture`, `Mesh`, `Material`, `Sprite`, `Font` and other `ReferResource` types) | Share the reference — the clone uses the same asset as the source. |
| `Entity` / `Component` references | Automatically remapped to the corresponding clone within the cloned subtree; references pointing outside the subtree keep the original reference. |
| Math value types (`Vector2`, `Vector3`, `Vector4`, `Quaternion`, `Matrix`, `Color`, ...) and other value-semantic data | Deep cloned — the clone gets a fully independent copy. |
| Containers (`Array`, `Map`, `Set`, TypedArray, `DataView`, plain objects) | Deep cloned — a fresh container is created, and each member (for `Map`, keys included) is cloned again according to its own type semantics. |
| Runtime containers (internal transient state such as `UpdateFlagManager`) | Ignored — the clone keeps its own constructor-built value. |
| Function values | The clone keeps its own constructor-built function; if it has none, the source's function is shared. |
| Other objects | Share the reference (assignment). |

A class opts into the deep-clone default in one of two ways: extend [DataObject](/apis/core/#DataObject) (data classes — the engine clones them field by field), or expose a `copyFrom` method (value types like the math classes — the engine copies through it). Either way the type must support argument-less construction: when a slot has no reusable preset instance, the engine bare-constructs one with `new Type()` before populating it, and a constructor that cannot run bare throws.

So without any decorator, plain data objects and arrays get independent deep copies, assets remain shared, and entity references are automatically remapped:

```typescript
// define a custom script
class CustomScript extends Script {
/** Entity reference.*/
target: Entity;

/** Plain data object.*/
config = { speed: 1, offsets: [0, 1, 2] };

/** Asset.*/
texture: Texture2D;
}

// Init entity and script
const entity = engine.createEntity();
const child = entity.createChild("child");
const script = entity.addComponent(CustomScript);
script.target = child;
script.texture = new Texture2D(engine, 128, 128);

// Clone logic
const cloneEntity = entity.clone();
const cloneScript = cloneEntity.getComponent(CustomScript);
console.log(cloneScript.target === cloneEntity.findByName("child")); // output is true, entity references are remapped to the corresponding clone in the cloned subtree.
console.log(cloneScript.config === script.config); // output is false, plain data objects are deep cloned into independent copies.
console.log(cloneScript.texture === script.texture); // output is true, assets are shared between the source and the clone.
```

### Clone Decorators
In addition to the default cloning method, the engine also provides "clone decorators" to customize the cloning method for script fields. The engine has four built-in clone decorators:

In addition to the default type-driven rules, the engine also provides "clone decorators" to customize the cloning method for script fields. Field decorators have the highest priority — they override the value type's default cloning behavior and apply wherever the clone walks fields: at the component's top level and inside any object that is itself deep cloned (no field walk happens inside a value that stays shared, so no decorator is consulted there). The engine has three built-in clone decorators:

| Decorator Name | Decorator Description |
| :--- | :--- |
| [ignoreClone](/apis/core/#ignoreClone) | Ignore the field during cloning. |
| [assignmentClone](/apis/core/#assignmentClone) | (Default value, equivalent to not adding any clone decorator) Assign the field during cloning. If it is a basic type, the value will be copied; if it is a reference type, the reference address will be copied. |
| [shallowClone](/apis/core/#shallowClone) | Shallow clone the field during cloning. After cloning, it will maintain its own independent reference and clone all its internal fields by assignment (if the internal field is a basic type, the value will be copied; if the internal field is a reference type, the reference address will be copied). |
| [deepClone](/apis/core/#deepClone) | Deep clone the field during cloning. After cloning, it will maintain its own independent reference, and all its internal deep fields will remain completely independent. |
| :-- | :-- |
| [ignoreClone](/apis/core/#ignoreClone) | Ignore the field during cloning; the clone keeps its own constructor-built value. |
| [assignmentClone](/apis/core/#assignmentClone) | Assign the field during cloning. If it is a basic type, the value will be copied; if it is a reference type, the clone will share the reference with the source. |
| [deepClone](/apis/core/#deepClone) | Deep clone the field's whole subtree. The deep intent carries into members, while assets inside stay shared, entity references remap, and runtime state keeps the clone's own. Engine-bound values cannot be deep cloned: `@deepClone` on an `Entity` / `Component` reference, an asset, engine runtime state (such as `UpdateFlagManager`), or a function throws — remove the decorator to get the default behavior, or copy an asset via its own clone API. |

<Callout type="negative">
`@shallowClone` has been removed. Its old semantics — copy the container itself but share its members — no longer
exist. Migrate by intent: use `@deepClone` if the field should be independent, or `@assignmentClone` if it should be
shared.
</Callout>

We slightly modify the above example and add different "clone decorators" to the four fields in `CustomScript`. Since arrays are already deep cloned by default, we use `assignmentClone` on field `c` to force sharing, and `deepClone` on field `d` to make the default behavior explicit, with additional print output for further explanation.

We slightly modify the above example and add different "clone decorators" to the four fields in `CustomScript`. Since `shallowClone` and `deepClone` are more complex, we add additional print output to the fields `c` and `d` for further explanation.
```typescript
// define a custom script
class CustomScript extends Script{
class CustomScript extends Script {
/** boolean type.*/
@ignoreClone
a:boolean = false;
a: boolean = false;

/** number type.*/
@assignmentClone
b:number = 1;
b: number = 1;

/** class type.*/
@shallowClone
c:Vector3[] = [new Vector3(0,0,0)];
@assignmentClone
c: Vector3[] = [new Vector3(0, 0, 0)];

/** class type.*/
@deepClone
d:Vector3[] = [new Vector3(0,0,0)];
d: Vector3[] = [new Vector3(0, 0, 0)];
}

// Init entity and script
const entity = engine.createEntity();
const script = entity.addComponent(CustomScript);
script.a = true;
script.b = 2;
script.c[0].set(1,1,1);
script.d[0].set(1,1,1);
script.c[0].set(1, 1, 1);
script.d[0].set(1, 1, 1);

// Clone logic
const cloneEntity = entity.clone();
const cloneScript = cloneEntity.getComponent(CustomScript);
console.log(cloneScript.a); // output is false,ignoreClone will ignore the value.
console.log(cloneScript.a); // output is false,ignoreClone keeps the clone's own constructor-built value.
console.log(cloneScript.b); // output is 2,assignmentClone is just assignment the origin value.
console.log(cloneScript.c[0]); // output is Vector3(1,1,1),shallowClone clone the array shell,but use the same element.
console.log(cloneScript.c[0]); // output is Vector3(1,1,1),assignmentClone shares the same array instance with the source.
console.log(cloneScript.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element.

cloneScript.c[0].set(2,2,2); // change the field c[0] value to (2,2,2).
cloneScript.d[0].set(2,2,2); // change the field d[0] value to (2,2,2).
cloneScript.c[0].set(2, 2, 2); // change the field c[0] value to (2,2,2).
cloneScript.d[0].set(2, 2, 2); // change the field d[0] value to (2,2,2).

console.log(script.c[0]); // output is (2,2,2). bacause shallowClone let c[0] use the same reference with cloneScript's c[0].
console.log(script.c[0]); // output is (2,2,2). bacause assignmentClone let c use the same array reference with cloneScript's c.
console.log(script.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0].
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

- Note:

- `shallowClone` and `deepClone` are usually used for *Object*, *Array*, and *Class* types.
- `shallowClone` will maintain its own independent reference after cloning and clone all its internal fields by assignment (if the internal field is a basic type, the value will be copied; if the internal field is a reference type, the reference address will be copied).
- `deepClone` is a deep clone that will recursively clone the properties deeply. How the sub-properties of the properties are cloned depends on the decorators of the sub-properties.
- If the clone decorators do not meet the requirements, you can implement the [_cloneTo()](/apis/design/#IClone-cloneTo) method to add custom cloning.
- Field decorators have the highest priority and also apply to the fields of nested objects, wherever the clone walks fields.
- `deepClone` deep-clones the whole subtree: the intent carries into members, so a member class instance with no deep default of its own is copied too (it must support argument-less construction). Assets inside stay shared, entity references are remapped, runtime state keeps the clone's own, and nested field decorators still win.
- `deepClone` cannot deep clone engine-bound objects: `@deepClone` on an `Entity` / `Component` reference, an asset, engine runtime state, or a function throws, because the explicit intent can't be honored. Remove the decorator to fall back to the default, or use the asset's own clone API for a real copy.
- If the clone decorators do not meet the requirements, you can implement the [\_cloneTo()](/apis/design/#IClone-cloneTo) method to add custom cloning.

## Cloning and Asset Reference Counting

When a component's top-level field shares a ref-counted asset (a `ReferResource` such as `Texture`, `Mesh`, or `Material`) during cloning, the engine automatically adds one reference for the clone, so destroying the source never pulls the asset out from under it. Built-in components release their references when destroyed. An asset referenced by custom script fields is kept alive by its clones — destroy the asset itself via its `destroy()` when it is no longer needed.
4 changes: 2 additions & 2 deletions docs/en/how-to-contribute.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class CustomScript extends Script {
a:boolean = false;
@assignmentClone
b:number = 1;
@shallowClone
@deepClone
c:Vector3[] = [new Vector3(0,0,0)];
}
```
Expand All @@ -133,7 +133,7 @@ class CustomScript extends Script{
a:boolean = false;
@assignmentClone
b:number = 1;
@shallowClone
@deepClone
c:Vector3[] = [new Vector3(0,0,0)];
}

Expand Down
Loading
Loading