Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9cb3920
feat: limit physics delta time per scene
luzhuang Jul 1, 2026
099b637
fix: instantiate source-v2 glTF scene references
luzhuang Jul 10, 2026
4b65762
fix(physics): rebuild mesh shapes and scaled defaults
luzhuang Jun 17, 2026
3969fec
fix(physics): keep mesh recooking transactional
luzhuang Jun 17, 2026
7bcc2e1
fix(physics): harden cloned mesh ownership
luzhuang Jul 11, 2026
2552ce6
docs: record engine compatibility decisions
luzhuang Jul 12, 2026
6eb384c
fix: support source-v2 runtime serialization
luzhuang Jul 12, 2026
80c4bce
fix(physics): gate contact event buffering
luzhuang Jun 17, 2026
5391644
fix(physics): preserve kinematic sync semantics
luzhuang Jun 17, 2026
9eeb20a
fix(physics): rebuild mesh shapes and scaled defaults
luzhuang Jun 17, 2026
5b7bda9
chore: keep engineering notes local
luzhuang Jul 13, 2026
6cd7b21
chore(loader): remove asset extensions note
luzhuang Jul 13, 2026
fe0af8a
feat(loader): support constructed values and nested calls
luzhuang Jul 13, 2026
4252f17
fix: resolve migration runtime review findings
luzhuang Jul 14, 2026
2f0e8ef
refactor: isolate loader runtime contract
luzhuang Jul 14, 2026
f2203ee
feat: migrate shaderlab fixes to 2.0
Jul 19, 2026
79dbaa5
refactor(clone): adopt type-driven clone defaults
Jul 19, 2026
2fc9508
chore: release v0.0.0-experimental-2.0-migrate.1
Jul 19, 2026
768ba05
fix(core): preserve hierarchy updates when clearing children
Jul 21, 2026
9f6fe8e
chore: release v0.0.0-experimental-2.0-migrate.2
Jul 21, 2026
23b6e9d
fix(core): preserve Transform replacement dependencies
Jul 23, 2026
6e86f4d
style(core): brace Transform replacement check
Jul 23, 2026
f041bdb
chore: release v0.0.0-experimental-2.0-migrate.3
Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ tsconfig.tsbuildinfo
api
docs/api
docs/plans
/notes/
yarn.lock
e2e/videos/*
e2e/screenshots/*
Expand All @@ -47,4 +48,7 @@ CLAUDE.md

# For bison generated files used by ShaderLab
*.tab.c
*.output
*.output

# Local-only test assets (third-party game art, not for redistribution)
examples/public/spine/
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].
```

- 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