diff --git a/docs/en/core/clone.mdx b/docs/en/core/clone.mdx index 2453b0531d..9c06bf50b7 100644 --- a/docs/en/core/clone.mdx +++ b/docs/en/core/clone.mdx @@ -5,99 +5,164 @@ type: Core 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 is a common runtime feature. Cloning an entity also clones all its bound components. For example, during initialization you might dynamically create identical entities and place them at different positions according to game logic. ## 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. + +Call the entity's [clone()](/apis/core/#Entity-clone) method to clone the entity along with its entire subtree and all 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: +## Opt-in Property Model + +The clone system uses an **opt-in** model: only fields marked with the `@property` decorator participate in cloning. Unmarked fields keep their constructor-initialized default value in the clone. + ```typescript -// define a custom script -class CustomScript extends Script{ - /** boolean type.*/ - a:boolean = false; - - /** number type.*/ - b:number = 1; - - /** class type.*/ - c:Vector3 = new Vector3(0,0,0); +import { Script, property } from "@galacean/engine"; +import { Vector3 } from "@galacean/engine"; + +class MyScript extends Script { + // Cloned: marked with @property + @property + speed: number = 10; + + @property + offset: Vector3 = new Vector3(1, 0, 0); + + // NOT cloned: no decorator, keeps constructor default in the clone + private _cache: Map = new Map(); } +``` -// 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); +## Clone Modes -// 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). +HOW a `@property` field is cloned depends on its runtime value's type. The clone system inspects the value's `@defaultCloneMode` to determine the strategy: + +| Clone Mode | Behavior | Typical Types | +| :--- | :--- | :--- | +| `CloneMode.Deep` | Recursively deep-clone, producing an independent copy | Vector3, Color, custom classes (default) | +| `CloneMode.Assignment` | Share the same reference; ref-counted resources get +1 | Texture, Mesh, Material, AnimatorController | +| `CloneMode.Remap` | Resolve to the corresponding clone within the cloned subtree | Entity, Component | + +You do not need to specify the mode per field — it is determined automatically by the type of the value the field holds. + +### Deep Clone (default) + +When a `@property` field holds a plain object or a class without `@defaultCloneMode`, it is deep-cloned. Each clone gets its own independent copy: + +```typescript +class MyScript extends Script { + @property + offset: Vector3 = new Vector3(1, 0, 0); +} + +const clone = entity.clone(); +const cloneScript = clone.getComponent(MyScript); + +// Independent copy — modifying one does not affect the other +cloneScript.offset.set(2, 2, 2); +console.log(script.offset); // still (1, 0, 0) ``` -### 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: -| 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. | +### Assignment (Shared Reference) + +Asset types like `Material`, `Texture`, and `Mesh` are shared by reference. The clone acquires an additional reference that is released when the clone is destroyed: -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{ - /** boolean type.*/ - @ignoreClone - a:boolean = false; - - /** number type.*/ - @assignmentClone - b:number = 1; - - /** class type.*/ - @shallowClone - c:Vector3[] = [new Vector3(0,0,0)]; - - /** class type.*/ - @deepClone - d:Vector3[] = [new Vector3(0,0,0)]; +class MyScript extends Script { + @property + material: Material; } -// 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); +const clone = entity.clone(); +const cloneScript = clone.getComponent(MyScript); -// 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.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.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element. +// Same reference — both point to the same material asset +console.log(cloneScript.material === script.material); // true +``` + +### Remap (Entity / Component References) + +When a `@property` field holds an Entity or Component that exists within the cloned subtree, it automatically remaps to the corresponding clone: + +```typescript +class MyScript extends Script { + @property + target: Entity; -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). + @property + targetRenderer: MeshRenderer; +} + +const parent = scene.createRootEntity("parent"); +const child = parent.createChild("child"); +const renderer = child.addComponent(MeshRenderer); + +const script = parent.addComponent(MyScript); +script.target = child; +script.targetRenderer = renderer; + +const clonedParent = parent.clone(); +const clonedScript = clonedParent.getComponent(MyScript); -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.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0]. +// Remapped to the cloned child, not the original +console.log(clonedScript.target === clonedParent.children[0]); // true +console.log(clonedScript.targetRenderer === clonedParent.children[0].getComponent(MeshRenderer)); // true ``` -- 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. +If the referenced entity is **outside** the cloned subtree, the original reference is preserved as-is. + +## @defaultCloneMode Decorator + +Use the `@defaultCloneMode` class decorator to control how instances of your custom type behave when they appear in `@property` fields: + +```typescript +import { CloneMode, defaultCloneMode } from "@galacean/engine"; + +// Instances of SharedConfig will be shared (not deep-cloned) across clones +@defaultCloneMode(CloneMode.Assignment) +class SharedConfig { + theme: string = "default"; + maxRetries: number = 3; +} + +class MyScript extends Script { + @property + config: SharedConfig; // Shared by reference due to @defaultCloneMode +} +``` + +Built-in types already have appropriate defaults — you only need `@defaultCloneMode` for your own custom types that should be shared or remapped. + +## Custom Clone Hook + +For advanced scenarios where cloned fields alone are not sufficient (e.g., rebuilding native objects, syncing derived state), implement the `_cloneTo` method: + +```typescript +class MyScript extends Script { + @property + private _radius: number = 1; + + private _nativeShape: NativeShape; // Not cloned (transient) + + _cloneTo(target: MyScript): void { + // Rebuild native state from the cloned fields + target._nativeShape = NativeShape.create(target._radius); + } +} +``` + +The `_cloneTo` hook is called **after** all `@property` fields have been populated on the clone. It receives the cloned instance as its argument. + +## Quick Reference + +| Field Type | Approach | Example | +| :--- | :--- | :--- | +| Primitive (number, string, boolean) | `@property` — copied by value | `@property speed = 10` | +| Value type (Vector3, Color, Quaternion) | `@property` — deep-cloned independently | `@property offset = new Vector3()` | +| Asset (Material, Texture, Mesh) | `@property` — shared by reference | `@property material: Material` | +| Entity / Component reference | `@property` — remapped to clone | `@property target: Entity` | +| Transient / cached state | No decorator — not cloned | `private _cache = new Map()` | +| Array / Map / Set of values | `@property` — deep-cloned (contents follow their own type rules) | `@property items: Vector3[] = []` | diff --git a/docs/zh/core/clone.mdx b/docs/zh/core/clone.mdx index 4cf66c382d..613bea18d6 100644 --- a/docs/zh/core/clone.mdx +++ b/docs/zh/core/clone.mdx @@ -5,101 +5,164 @@ type: 核心 label: Core --- +实体克隆是运行时的常用功能。克隆实体时会同时克隆其绑定的所有组件。例如在初始化阶段根据配置动态创建多个相同的实体,然后根据游戏逻辑放置到场景中不同的位置。 -节点克隆是运行时的常用功能,同时节点克隆也会附带克隆其绑定的组件。例如在初始化阶段根据配置动态创建一定数量相同的实体,然后根据逻辑规则摆放到场景不同的位置。这里会对脚本的克隆细节进行详细讲解。 +## 实体克隆 + +调用实体的 [clone()](/apis/core/#Entity-clone) 方法即可克隆实体及其整个子树和所有附属组件: -## 实体的克隆 -非常简单,直接调用实体的 [clone()](/apis/design/#IClone-clone) 方法即可完成实体以及附属组件的克隆。 ```typescript const cloneEntity = entity.clone(); ``` -## 脚本的克隆 -脚本的本质也是组件,所以当我们调用实体的 [clone()](/apis/design/#IClone-clone) 函数时,引擎不仅会对引擎内置组件进行克隆,还会对自定义脚本进行克隆。引擎内置组件的克隆规则官方已经完成定制,同样我们也将脚本的克隆能力和规则定制开放给了开发者。脚本字段默认的克隆方式为浅拷贝,例如我们对脚本的字段值进行修改后再克隆,克隆后的脚本将保持修改后的值,无需增加任何额外的编码。以下为自定义脚本的克隆案例: +## Opt-in 属性模型 + +克隆系统采用 **opt-in(显式标记)** 模型:只有标记了 `@property` 装饰器的字段才会参与克隆。未标记的字段在克隆体中保持构造函数的默认值。 + ```typescript -// define a custom script -class CustomScript extends Script{ - /** boolean type.*/ - a:boolean = false; - - /** number type.*/ - b:number = 1; - - /** class type.*/ - c:Vector3 = new Vector3(0,0,0); +import { Script, property } from "@galacean/engine"; +import { Vector3 } from "@galacean/engine"; + +class MyScript extends Script { + // 参与克隆:标记了 @property + @property + speed: number = 10; + + @property + offset: Vector3 = new Vector3(1, 0, 0); + + // 不参与克隆:没有装饰器,克隆体保持构造函数默认值 + private _cache: Map = new Map(); } +``` -// 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); +## 克隆模式 -// 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). +`@property` 字段**如何**被克隆,取决于其运行时值的类型。克隆系统通过检查值类型的 `@defaultCloneMode` 来决定策略: + +| 克隆模式 | 行为 | 典型类型 | +| :--- | :--- | :--- | +| `CloneMode.Deep` | 递归深克隆,生成独立副本 | Vector3、Color、自定义类(默认) | +| `CloneMode.Assignment` | 共享同一引用;引用计数资源 +1 | Texture、Mesh、Material、AnimatorController | +| `CloneMode.Remap` | 解析为克隆子树中对应的克隆体 | Entity、Component | + +无需在字段上指定模式——系统会根据字段持有的值的类型自动判断。 + +### 深克隆(默认) + +当 `@property` 字段持有普通对象或没有 `@defaultCloneMode` 的类实例时,会被深克隆。每个克隆体获得自己的独立副本: + +```typescript +class MyScript extends Script { + @property + offset: Vector3 = new Vector3(1, 0, 0); +} + +const clone = entity.clone(); +const cloneScript = clone.getComponent(MyScript); + +// 独立副本——修改一个不会影响另一个 +cloneScript.offset.set(2, 2, 2); +console.log(script.offset); // 仍然是 (1, 0, 0) ``` -### 克隆装饰器 -除了默认的克隆方式外,引擎还提供了“克隆装饰器“对脚本字段的克隆方式进行定制。引擎内置四种克隆装饰: -| 装饰器名称 | 装饰器释义 | -| :--- | :--- | -| [ignoreClone](/apis/core/#ignoreClone) | 克隆时对字段进行忽略。 | -| [assignmentClone](/apis/core/#assignmentClone) | ( 默认值,和不添加任何克隆装饰器等效) 克隆时对字段进行赋值。如果是基本类型则会拷贝值,如果是引用类型则会拷贝其引用地址。 | -| [shallowClone](/apis/core/#shallowClone) | 克隆时对字段进行浅克隆。克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。| -| [deepClone](/apis/core/#deepClone) | 克隆时对字段进行深克隆。克隆后会保持自身引用独立,并且其内部所有深层字段均保持完全独立。| +### 引用共享(Assignment) + +`Material`、`Texture`、`Mesh` 等资源类型通过引用共享。克隆体获得一个额外的引用,在销毁时释放: -我们将上面的案例稍加修改,分别对 `CustomScript` 中的四个字段添加了不同的“克隆装饰器“。由于 `shallowClone` 和 `deepCone`  较复杂,我们对字段 `c` 和 `d` 增加了额外的打印输出进行进一步讲解。 ```typescript -// define a custom script -class CustomScript extends Script{ - /** boolean type.*/ - @ignoreClone - a:boolean = false; - - /** number type.*/ - @assignmentClone - b:number = 1; - - /** class type.*/ - @shallowClone - c:Vector3[] = [new Vector3(0,0,0)]; - - /** class type.*/ - @deepClone - d:Vector3[] = [new Vector3(0,0,0)]; +class MyScript extends Script { + @property + material: Material; } -// 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); +const clone = entity.clone(); +const cloneScript = clone.getComponent(MyScript); -// 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.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.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element. +// 同一引用——两者指向同一个 material 资源 +console.log(cloneScript.material === script.material); // true +``` + +### 引用重映射(Remap) + +当 `@property` 字段持有的 Entity 或 Component 存在于被克隆的子树中时,会自动重映射到对应的克隆体: + +```typescript +class MyScript extends Script { + @property + target: Entity; -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). + @property + targetRenderer: MeshRenderer; +} + +const parent = scene.createRootEntity("parent"); +const child = parent.createChild("child"); +const renderer = child.addComponent(MeshRenderer); + +const script = parent.addComponent(MyScript); +script.target = child; +script.targetRenderer = renderer; + +const clonedParent = parent.clone(); +const clonedScript = clonedParent.getComponent(MyScript); + +// 已重映射到克隆的 child,而非原始对象 +console.log(clonedScript.target === clonedParent.children[0]); // true +console.log(clonedScript.targetRenderer === clonedParent.children[0].getComponent(MeshRenderer)); // true +``` + +如果引用的实体在被克隆子树**之外**,则保持原始引用不变。 + +## @defaultCloneMode 装饰器 + +使用 `@defaultCloneMode` 类装饰器来控制自定义类型的实例在 `@property` 字段中的克隆行为: + +```typescript +import { CloneMode, defaultCloneMode } from "@galacean/engine"; + +// SharedConfig 的实例在克隆时将被共享(而非深克隆) +@defaultCloneMode(CloneMode.Assignment) +class SharedConfig { + theme: string = "default"; + maxRetries: number = 3; +} + +class MyScript extends Script { + @property + config: SharedConfig; // 由于 @defaultCloneMode,通过引用共享 +} +``` + +引擎内置类型已经设定了合适的默认模式——只有自定义类型需要使用 `@defaultCloneMode`。 + +## 自定义克隆钩子 -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.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0]. +对于克隆字段不足以完成状态恢复的高级场景(如重建原生对象、同步派生状态),可以实现 `_cloneTo` 方法: + +```typescript +class MyScript extends Script { + @property + private _radius: number = 1; + + private _nativeShape: NativeShape; // 不参与克隆(瞬态状态) + + _cloneTo(target: MyScript): void { + // 从已克隆的字段重建原生状态 + target._nativeShape = NativeShape.create(target._radius); + } +} ``` -- 注意: - - `shallowClone` 和 `deepClone` 通常用于 *Object*、*Array* 和 *Class* 类型。 - - `shallowClone` 克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。 - - `deepClone` 为深克隆,会对属性进行深度递归,至于属性的子属性如何克隆,取决于子属性的装饰器。 - - 如果克隆装饰器不能满足诉求,可以通过实现 [_cloneTo()](/apis/design/#IClone-cloneTo) 方法追加自定义克隆。 +`_cloneTo` 钩子在克隆体的所有 `@property` 字段填充完成**之后**调用,参数为克隆实例。 + +## 快速参考 +| 字段类型 | 方式 | 示例 | +| :--- | :--- | :--- | +| 基本类型(number、string、boolean) | `@property` — 按值复制 | `@property speed = 10` | +| 值类型(Vector3、Color、Quaternion) | `@property` — 深克隆为独立副本 | `@property offset = new Vector3()` | +| 资源(Material、Texture、Mesh) | `@property` — 引用共享 | `@property material: Material` | +| Entity / Component 引用 | `@property` — 重映射到克隆体 | `@property target: Entity` | +| 瞬态 / 缓存状态 | 不加装饰器 — 不参与克隆 | `private _cache = new Map()` | +| Array / Map / Set | `@property` — 深克隆(内容遵循各自类型规则) | `@property items: Vector3[] = []` | diff --git a/packages/core/src/2d/sprite/Sprite.ts b/packages/core/src/2d/sprite/Sprite.ts index 5232001c9a..28e9cb0b65 100644 --- a/packages/core/src/2d/sprite/Sprite.ts +++ b/packages/core/src/2d/sprite/Sprite.ts @@ -2,7 +2,6 @@ import { BoundingBox, MathUtil, Rect, Vector2, Vector4 } from "@galacean/engine- import { Engine } from "../../Engine"; import { UpdateFlagManager } from "../../UpdateFlagManager"; import { ReferResource } from "../../asset/ReferResource"; -import { ignoreClone } from "../../clone/CloneManager"; import { Texture2D } from "../../texture/Texture2D"; import { SpriteAtlas } from "../atlas/SpriteAtlas"; import { SpriteModifyFlags } from "../enums/SpriteModifyFlags"; @@ -382,7 +381,6 @@ export class Sprite extends ReferResource { this._updateFlagManager.dispatch(type); } - @ignoreClone private _onRegionChange(): void { const { _region: region } = this; // @ts-ignore @@ -398,12 +396,10 @@ export class Sprite extends ReferResource { region._onValueChanged = this._onRegionChange; } - @ignoreClone private _onPivotChange(): void { this._dispatchSpriteChange(SpriteModifyFlags.pivot); } - @ignoreClone private _onBorderChange(): void { const { _border: border } = this; // @ts-ignore diff --git a/packages/core/src/2d/sprite/SpriteMask.ts b/packages/core/src/2d/sprite/SpriteMask.ts index 1f740081f9..7824f15056 100644 --- a/packages/core/src/2d/sprite/SpriteMask.ts +++ b/packages/core/src/2d/sprite/SpriteMask.ts @@ -6,7 +6,7 @@ import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; -import { assignmentClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { SpriteMaskLayer } from "../../enums/SpriteMaskLayer"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; @@ -24,36 +24,30 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { static _alphaCutoffProperty: ShaderProperty = ShaderProperty.getByName("renderer_MaskAlphaCutoff"); /** The mask layers the sprite mask influence to. */ - @assignmentClone + @property influenceLayers: SpriteMaskLayer = SpriteMaskLayer.Everything; /** @internal */ - @ignoreClone _renderElement: RenderElement; /** @internal */ - @ignoreClone _subChunk: SubPrimitiveChunk; /** @internal */ - @ignoreClone _maskIndex: number = -1; - @ignoreClone private _sprite: Sprite = null; - @ignoreClone private _automaticWidth: number = 0; - @ignoreClone private _automaticHeight: number = 0; - @assignmentClone + @property private _customWidth: number = undefined; - @assignmentClone + @property private _customHeight: number = undefined; - @assignmentClone + @property private _flipX: boolean = false; - @assignmentClone + @property private _flipY: boolean = false; - @assignmentClone + @property private _alphaCutoff: number = 0.5; /** @@ -333,7 +327,6 @@ export class SpriteMask extends Renderer implements ISpriteRenderer { this._dirtyUpdateFlag &= ~SpriteMaskUpdateFlags.AutomaticSize; } - @ignoreClone private _onSpriteChange(type: SpriteModifyFlags): void { switch (type) { case SpriteModifyFlags.texture: diff --git a/packages/core/src/2d/sprite/SpriteRenderer.ts b/packages/core/src/2d/sprite/SpriteRenderer.ts index 2a6986af6a..5d0829ede8 100644 --- a/packages/core/src/2d/sprite/SpriteRenderer.ts +++ b/packages/core/src/2d/sprite/SpriteRenderer.ts @@ -6,7 +6,7 @@ import { RenderContext } from "../../RenderPipeline/RenderContext"; import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../../Renderer"; -import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ISpriteAssembler } from "../assembler/ISpriteAssembler"; import { ISpriteRenderer } from "../assembler/ISpriteRenderer"; @@ -28,34 +28,28 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { static _textureProperty: ShaderProperty = ShaderProperty.getByName("renderer_SpriteTexture"); /** @internal */ - @ignoreClone _subChunk: SubPrimitiveChunk; - @ignoreClone private _drawMode: SpriteDrawMode; - @ignoreClone private _assembler: ISpriteAssembler; - @assignmentClone + @property private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; - @assignmentClone + @property private _tiledAdaptiveThreshold: number = 0.5; - @deepClone + @property private _color: Color = new Color(1, 1, 1, 1); - @ignoreClone private _sprite: Sprite = null; - @ignoreClone private _automaticWidth: number = 0; - @ignoreClone private _automaticHeight: number = 0; - @assignmentClone + @property private _customWidth: number = undefined; - @assignmentClone + @property private _customHeight: number = undefined; - @assignmentClone + @property private _flipX: boolean = false; - @assignmentClone + @property private _flipY: boolean = false; /** @@ -412,7 +406,6 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { this._dirtyUpdateFlag &= ~SpriteRendererUpdateFlags.AutomaticSize; } - @ignoreClone private _onSpriteChange(type: SpriteModifyFlags): void { switch (type) { case SpriteModifyFlags.texture: @@ -467,7 +460,6 @@ export class SpriteRenderer extends Renderer implements ISpriteRenderer { } } - @ignoreClone private _onColorChanged(): void { this._dirtyUpdateFlag |= SpriteRendererUpdateFlags.Color; } diff --git a/packages/core/src/2d/text/SubFont.ts b/packages/core/src/2d/text/SubFont.ts index d9a03919e2..cdbb88518f 100644 --- a/packages/core/src/2d/text/SubFont.ts +++ b/packages/core/src/2d/text/SubFont.ts @@ -5,10 +5,13 @@ import { Texture2D } from "../../texture"; import { FontAtlas } from "../atlas/FontAtlas"; import { CharInfo } from "./CharInfo"; import { TextUtils } from "./TextUtils"; +import { CloneMode } from "../../clone/enums/CloneMode"; +import { defaultCloneMode } from "../../clone/CloneManager"; /** * @internal */ +@defaultCloneMode(CloneMode.Assignment) export class SubFont { nativeFontString: string; diff --git a/packages/core/src/2d/text/TextRenderer.ts b/packages/core/src/2d/text/TextRenderer.ts index 3649180c79..4e9e632bd3 100644 --- a/packages/core/src/2d/text/TextRenderer.ts +++ b/packages/core/src/2d/text/TextRenderer.ts @@ -8,7 +8,7 @@ import { SubPrimitiveChunk } from "../../RenderPipeline/SubPrimitiveChunk"; import { RenderElement } from "../../RenderPipeline/RenderElement"; import { Renderer } from "../../Renderer"; import { TransformModifyFlags } from "../../Transform"; -import { assignmentClone, deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderProperty } from "../../shader"; import { ShaderDataGroup } from "../../shader/enums/ShaderDataGroup"; import { Texture2D } from "../../texture"; @@ -33,41 +33,38 @@ export class TextRenderer extends Renderer implements ITextRenderer { private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; - @ignoreClone private _textChunks = Array(); /** @internal */ - @assignmentClone + @property _subFont: SubFont = null; /** @internal */ - @ignoreClone _dirtyFlag = DirtyFlag.Font; - @deepClone + @property private _color = new Color(1, 1, 1, 1); - @assignmentClone + @property private _text = ""; - @assignmentClone + @property private _width = 0; - @assignmentClone + @property private _height = 0; - @ignoreClone private _localBounds = new BoundingBox(); - @assignmentClone + @property private _font: Font = null; - @assignmentClone + @property private _fontSize = 24; - @assignmentClone + @property private _fontStyle = FontStyle.None; - @assignmentClone + @property private _lineSpacing = 0; - @assignmentClone + @property private _characterSpacing = 0; - @assignmentClone + @property private _horizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone + @property private _verticalAlignment = TextVerticalAlignment.Center; - @assignmentClone + @property private _enableWrapping = false; - @assignmentClone + @property private _overflowMode = OverflowMode.Overflow; /** @@ -330,15 +327,6 @@ export class TextRenderer extends Renderer implements ITextRenderer { this._subFont && (this._subFont = null); } - /** - * @internal - */ - override _cloneTo(target: TextRenderer): void { - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - } - /** * @internal */ @@ -676,7 +664,6 @@ export class TextRenderer extends Renderer implements ITextRenderer { charRenderInfos.length = 0; } - @ignoreClone protected override _onTransformChanged(bit: TransformModifyFlags): void { super._onTransformChanged(bit); this._setDirtyFlagTrue(DirtyFlag.WorldPosition | DirtyFlag.WorldBounds); @@ -741,7 +728,6 @@ export class TextRenderer extends Renderer implements ITextRenderer { textChunks.length = 0; } - @ignoreClone private _onColorChanged(): void { this._setDirtyFlagTrue(DirtyFlag.Color); } diff --git a/packages/core/src/Camera.ts b/packages/core/src/Camera.ts index 55d904f6c9..38be56ff82 100644 --- a/packages/core/src/Camera.ts +++ b/packages/core/src/Camera.ts @@ -9,7 +9,7 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { VirtualCamera } from "./VirtualCamera"; import { GLCapabilityType, Logger } from "./base"; -import { deepClone, ignoreClone } from "./clone/CloneManager"; +import { property } from "./clone/CloneManager"; import { AntiAliasing } from "./enums/AntiAliasing"; import { CameraClearFlags } from "./enums/CameraClearFlags"; import { CameraModifyFlags } from "./enums/CameraModifyFlags"; @@ -54,6 +54,7 @@ export class Camera extends Component { private static _cameraDepthBufferParamsProperty = ShaderProperty.getByName("camera_DepthBufferParams"); /** Whether to enable frustum culling, it is enabled by default. */ + @property enableFrustumCulling: boolean = true; /** @@ -61,17 +62,20 @@ export class Camera extends Component { * * @defaultValue `CameraClearFlags.All` */ + @property clearFlags: CameraClearFlags = CameraClearFlags.All; /** * Culling mask - which layers the camera renders. * @remarks Support bit manipulation, corresponding to `Layer`. */ + @property cullingMask: Layer = Layer.Everything; /** * Determines which PostProcess to use. */ + @property postProcessMask: Layer = Layer.Everything; /** @@ -80,6 +84,7 @@ export class Camera extends Component { * * @defaultValue `DepthTextureMode.None` */ + @property depthTextureMode: DepthTextureMode = DepthTextureMode.None; /** @@ -87,6 +92,7 @@ export class Camera extends Component { * * @defaultValue `Downsampling.TwoX` */ + @property opaqueTextureDownsampling: Downsampling = Downsampling.TwoX; /** @@ -95,6 +101,7 @@ export class Camera extends Component { * * @defaultValue `AntiAliasing.None` */ + @property antiAliasing: AntiAliasing = AntiAliasing.None; /** @@ -107,66 +114,72 @@ export class Camera extends Component { * * @defaultValue `false` */ + @property isAlphaOutputRequired = false; /** @internal */ - @ignoreClone _cameraType: CameraType = CameraType.Normal; /** @internal */ - @ignoreClone _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); /** @internal */ - @deepClone + @property _frustum: BoundingFrustum = new BoundingFrustum(); /** @internal */ - @ignoreClone _renderPipeline: BasicRenderPipeline; /** @internal */ - @deepClone + @property _virtualCamera: VirtualCamera = new VirtualCamera(); /** @internal */ + @property _replacementShader: Shader = null; /** @internal */ + @property _replacementSubShaderTag: ShaderTagKey = null; /** @internal */ + @property _replacementFailureStrategy: ReplacementFailureStrategy = null; /** @internal */ - @ignoreClone _cameraIndex: number = -1; + @property private _priority: number = 0; + @property private _isCustomViewMatrix = false; + @property private _isCustomProjectionMatrix = false; + @property private _fieldOfView: number = 45; + @property private _orthographicSize: number = 10; private _isProjectionDirty = true; private _isInvProjMatDirty: boolean = true; + @property private _customAspectRatio: number | undefined = undefined; + @property private _opaqueTextureEnabled: boolean = false; + @property private _enableHDR = false; + @property private _enablePostProcess = false; + @property private _msaaSamples: MSAASamples; + @property private _renderTarget: RenderTarget = null; - @ignoreClone private _updateFlagManager: UpdateFlagManager; - @ignoreClone private _frustumChangeFlag: BoolUpdateFlag; - @ignoreClone private _isViewMatrixDirty: BoolUpdateFlag; - @ignoreClone private _isInvViewProjDirty: BoolUpdateFlag; - @deepClone + @property private _shaderData: ShaderData = new ShaderData(ShaderDataGroup.Camera); - @ignoreClone private _depthBufferParams: Vector4 = new Vector4(); - @deepClone + @property private _viewport: Vector4 = new Vector4(0, 0, 1, 1); - @deepClone + @property private _pixelViewport: Rect = new Rect(0, 0, 0, 0); - @deepClone + @property private _inverseProjectionMatrix: Matrix = new Matrix(); - @deepClone + @property private _invViewProjMat: Matrix = new Matrix(); /** @@ -712,7 +725,12 @@ export class Camera extends Component { replacementTag?: string | ShaderTagKey, failureStrategy: ReplacementFailureStrategy = ReplacementFailureStrategy.KeepOriginalShader ): void { - this._replacementShader = shader; + const lastShader = this._replacementShader; + if (lastShader !== shader) { + lastShader && this._addResourceReferCount(lastShader, -1); + shader && this._addResourceReferCount(shader, 1); + this._replacementShader = shader; + } this._replacementSubShaderTag = typeof replacementTag === "string" ? ShaderTagKey.getByName(replacementTag) : replacementTag; this._replacementFailureStrategy = failureStrategy; @@ -722,6 +740,7 @@ export class Camera extends Component { * Reset and clear the replacement shader. */ resetReplacementShader(): void { + this._replacementShader && this._addResourceReferCount(this._replacementShader, -1); this._replacementShader = null; this._replacementSubShaderTag = null; this._replacementFailureStrategy = null; @@ -826,13 +845,6 @@ export class Camera extends Component { this._updateFlagManager?.removeListener(onChange); } - /** - * @internal - */ - _cloneTo(target: Camera): void { - this._renderTarget?._addReferCount(1); - } - /** * @internal * @inheritdoc @@ -844,10 +856,14 @@ export class Camera extends Component { this._isViewMatrixDirty.destroy(); this._addResourceReferCount(this.shaderData, -1); this._renderTarget && this._addResourceReferCount(this._renderTarget, -1); + this._replacementShader && this._addResourceReferCount(this._replacementShader, -1); //@ts-ignore this._viewport._onValueChanged = null; this.engine.canvas._sizeUpdateFlagManager.removeListener(this._onPixelViewportChanged); + this._replacementShader = null; + this._replacementSubShaderTag = null; + this._replacementFailureStrategy = null; this._globalShaderMacro = null; this._frustum = null; this._renderPipeline = null; @@ -958,7 +974,6 @@ export class Camera extends Component { return this._inverseProjectionMatrix; } - @ignoreClone private _onPixelViewportChanged(): void { this._updatePixelViewport(); this._customAspectRatio ?? this._projectionMatrixChange(); diff --git a/packages/core/src/Component.ts b/packages/core/src/Component.ts index 2ebafbcd05..ae89285fe3 100644 --- a/packages/core/src/Component.ts +++ b/packages/core/src/Component.ts @@ -1,7 +1,7 @@ import { IReferable } from "./asset/IReferable"; import { EngineObject } from "./base"; -import { assignmentClone, ignoreClone } from "./clone/CloneManager"; -import { CloneUtils } from "./clone/CloneUtils"; +import { defaultCloneMode, property } from "./clone/CloneManager"; +import { CloneMode } from "./clone/enums/CloneMode"; import { Entity } from "./Entity"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { Scene } from "./Scene"; @@ -9,21 +9,19 @@ import { Scene } from "./Scene"; /** * The base class of the components. */ +@defaultCloneMode(CloneMode.Remap) export class Component extends EngineObject { /** @internal */ _entity: Entity; /** @internal */ - @ignoreClone _awoken: boolean = false; - @ignoreClone protected _phasedActiveInScene: boolean = false; - @ignoreClone private _phasedActive: boolean = false; - @assignmentClone + @property private _enabled: boolean = true; /** @@ -154,13 +152,6 @@ export class Component extends EngineObject { } } - /** - * @internal - */ - _remap(srcRoot: Entity, targetRoot: Entity): T { - return CloneUtils.remapComponent(srcRoot, targetRoot, this) as unknown as T; - } - protected _addResourceReferCount(resource: IReferable, count: number): void { this._entity._isTemplate || resource._addReferCount(count); } diff --git a/packages/core/src/Entity.ts b/packages/core/src/Entity.ts index 063fbe1c1c..923245c71d 100644 --- a/packages/core/src/Entity.ts +++ b/packages/core/src/Entity.ts @@ -10,8 +10,9 @@ import { Transform } from "./Transform"; import { UpdateFlagManager } from "./UpdateFlagManager"; import { ReferResource } from "./asset/ReferResource"; import { EngineObject } from "./base"; -import { CloneUtils } from "./clone/CloneUtils"; import { ComponentCloner } from "./clone/ComponentCloner"; +import { CloneMode } from "./clone/enums/CloneMode"; +import { defaultCloneMode } from "./clone/CloneManager"; import { ActiveChangeFlag } from "./enums/ActiveChangeFlag"; import { EntityModifyFlags } from "./enums/EntityModifyFlags"; import { DisorderedArray } from "./utils/DisorderedArray"; @@ -19,6 +20,7 @@ import { DisorderedArray } from "./utils/DisorderedArray"; /** * Entity, be used as components container. */ +@defaultCloneMode(CloneMode.Remap) export class Entity extends EngineObject { /** @internal */ static _tempComponentConstructors: ComponentConstructor[] = []; @@ -420,8 +422,9 @@ export class Entity extends EngineObject { * @returns Cloned entity */ clone(): Entity { - const cloneEntity = this._createCloneEntity(); - this._parseCloneEntity(this, cloneEntity, this, cloneEntity, new Map()); + const cloneMap = new Map(); + const cloneEntity = this._createCloneEntity(cloneMap); + this._parseCloneEntity(this, cloneEntity, cloneMap); return cloneEntity; } @@ -433,13 +436,6 @@ export class Entity extends EngineObject { return this._updateFlagManager.createFlag(BoolUpdateFlag); } - /** - * @internal - */ - _remap(srcRoot: Entity, targetRoot: Entity): Entity { - return CloneUtils.remapEntity(srcRoot, targetRoot, this); - } - /** * @internal */ @@ -448,7 +444,7 @@ export class Entity extends EngineObject { this._templateResource = templateResource; } - private _createCloneEntity(): Entity { + private _createCloneEntity(cloneMap: Map): Entity { const componentConstructors = Entity._tempComponentConstructors; const components = this._components; for (let i = 0, n = components.length; i < n; i++) { @@ -456,6 +452,16 @@ export class Entity extends EngineObject { } const cloneEntity = new Entity(this.engine, this.name, ...componentConstructors); componentConstructors.length = 0; + + // Seed the identity map: source entity/component → its clone counterpart. + // Consumed during the data-fill pass to resolve Entity/Component refs (CloneMode.Remap). + // Relies on the component-order alignment from passing the same constructors in order. + cloneMap.set(this, cloneEntity); + const cloneComponents = cloneEntity._components; + for (let i = 0, n = components.length; i < n; i++) { + cloneMap.set(components[i], cloneComponents[i]); + } + const templateResource = this._templateResource; if (templateResource) { cloneEntity._templateResource = templateResource; @@ -466,27 +472,21 @@ export class Entity extends EngineObject { cloneEntity._isActive = this._isActive; const srcChildren = this._children; for (let i = 0, n = srcChildren.length; i < n; i++) { - cloneEntity.addChild(srcChildren[i]._createCloneEntity()); + cloneEntity.addChild(srcChildren[i]._createCloneEntity(cloneMap)); } return cloneEntity; } - private _parseCloneEntity( - src: Entity, - target: Entity, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { + private _parseCloneEntity(src: Entity, target: Entity, cloneMap: Map): void { const srcChildren = src._children; const targetChildren = target._children; for (let i = 0, n = srcChildren.length; i < n; i++) { - this._parseCloneEntity(srcChildren[i], targetChildren[i], srcRoot, targetRoot, deepInstanceMap); + this._parseCloneEntity(srcChildren[i], targetChildren[i], cloneMap); } const components = src._components; for (let i = 0, n = components.length; i < n; i++) { - ComponentCloner.cloneComponent(components[i], target._components[i], srcRoot, targetRoot, deepInstanceMap); + ComponentCloner.cloneComponent(components[i], target._components[i], cloneMap); } } diff --git a/packages/core/src/Renderer.ts b/packages/core/src/Renderer.ts index 5af1e71750..28bbd5157a 100644 --- a/packages/core/src/Renderer.ts +++ b/packages/core/src/Renderer.ts @@ -7,7 +7,7 @@ import { Entity } from "./Entity"; import { RenderContext } from "./RenderPipeline/RenderContext"; import { RenderElement } from "./RenderPipeline/RenderElement"; import { Transform, TransformModifyFlags } from "./Transform"; -import { assignmentClone, deepClone, ignoreClone } from "./clone/CloneManager"; +import { property } from "./clone/CloneManager"; import { SpriteMaskLayer } from "./enums/SpriteMaskLayer"; import { Material } from "./material"; import { ShaderMacro, ShaderProperty } from "./shader"; @@ -34,50 +34,36 @@ export class Renderer extends Component { private static _normalMatrixProperty = ShaderProperty.getByName("renderer_NormalMat"); /** @internal */ - @ignoreClone _distanceForSort: number; /** @internal */ - @ignoreClone _onUpdateIndex: number = -1; /** @internal */ - @ignoreClone _rendererIndex: number = -1; /** @internal */ - @ignoreClone _globalShaderMacro: ShaderMacroCollection = new ShaderMacroCollection(); - @ignoreClone _renderFrameCount: number; /** @internal */ - @assignmentClone + @property _maskInteraction: SpriteMaskInteraction = SpriteMaskInteraction.None; - @assignmentClone + @property _maskLayer: SpriteMaskLayer = SpriteMaskLayer.Layer0; - @ignoreClone protected _overrideUpdate: boolean = false; - @ignoreClone protected _materials: Material[] = []; - @ignoreClone protected _dirtyUpdateFlag: number = 0; - @ignoreClone protected _rendererLayer: Vector4 = new Vector4(); - @ignoreClone protected _bounds: BoundingBox = new BoundingBox(); protected _transformEntity: Entity; - @deepClone + @property private _shaderData: ShaderData = new ShaderData(ShaderDataGroup.Renderer); - @ignoreClone private _mvMatrix: Matrix = new Matrix(); - @ignoreClone private _mvpMatrix: Matrix = new Matrix(); - @ignoreClone private _normalMatrix: Matrix = new Matrix(); - @ignoreClone private _materialsInstanced: boolean[] = []; - @assignmentClone + @property private _priority: number = 0; - @assignmentClone + @property private _receiveShadows: boolean = true; /** @@ -113,6 +99,7 @@ export class Renderer extends Component { } /** Whether cast shadow. */ + @property castShadows: boolean = true; /** @@ -496,7 +483,6 @@ export class Renderer extends Component { } } - @ignoreClone protected _onTransformChanged(type: TransformModifyFlags): void { this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } diff --git a/packages/core/src/Script.ts b/packages/core/src/Script.ts index 997901573d..41288caa79 100644 --- a/packages/core/src/Script.ts +++ b/packages/core/src/Script.ts @@ -1,6 +1,5 @@ import { Camera } from "./Camera"; import { Component } from "./Component"; -import { ignoreClone } from "./clone/CloneManager"; import { PointerEventData } from "./input/pointer/PointerEventData"; import { ColliderShape } from "./physics"; import { Collision } from "./physics/Collision"; @@ -10,28 +9,20 @@ import { Collision } from "./physics/Collision"; */ export class Script extends Component { /** @internal */ - @ignoreClone _started: boolean = false; /** @internal */ - @ignoreClone _onStartIndex: number = -1; /** @internal */ - @ignoreClone _onUpdateIndex: number = -1; /** @internal */ - @ignoreClone _onLateUpdateIndex: number = -1; /** @internal */ - @ignoreClone _onPhysicsUpdateIndex: number = -1; /** @internal */ - @ignoreClone _onPreRenderIndex: number = -1; /** @internal */ - @ignoreClone _onPostRenderIndex: number = -1; /** @internal */ - @ignoreClone _entityScriptsIndex: number = -1; /** diff --git a/packages/core/src/Signal.ts b/packages/core/src/Signal.ts index a18b86cf5a..a860464175 100644 --- a/packages/core/src/Signal.ts +++ b/packages/core/src/Signal.ts @@ -1,7 +1,5 @@ import { Component } from "./Component"; -import { Entity } from "./Entity"; -import { CloneUtils } from "./clone/CloneUtils"; -import { ignoreClone } from "./clone/CloneManager"; + import { SafeLoopArray } from "./utils/SafeLoopArray"; /** @@ -9,7 +7,6 @@ import { SafeLoopArray } from "./utils/SafeLoopArray"; * @typeParam T - Tuple type of the signal arguments */ export class Signal { - @ignoreClone private _listeners: SafeLoopArray> = new SafeLoopArray>(); /** @@ -124,14 +121,17 @@ export class Signal { * @internal * Clone listeners to target signal, remapping entity/component references. */ - _cloneTo(target: Signal, srcRoot: Entity, targetRoot: Entity): void { + _cloneTo(target: Signal, cloneMap: Map): void { const listeners = this._listeners.getLoopArray(); for (let i = 0, n = listeners.length; i < n; i++) { const listener = listeners[i]; if (listener.destroyed || !listener.methodName) continue; - const clonedTarget = CloneUtils.remapComponent(srcRoot, targetRoot, listener.target); + // A target outside the cloned subtree is not in the map and is intentionally kept as-is + // (the clone's signal keeps notifying the original component). The guard below only filters + // a null-target binding — it can no longer mean "remap failed". + const clonedTarget = (cloneMap.get(listener.target) ?? listener.target) as Component; if (clonedTarget) { - const clonedArgs = this._cloneArguments(listener.arguments, srcRoot, targetRoot); + const clonedArgs = this._cloneArguments(listener.arguments, cloneMap); if (listener.once) { target.once(clonedTarget, listener.methodName, ...clonedArgs); } else { @@ -141,19 +141,15 @@ export class Signal { } } - private _cloneArguments(args: any[], srcRoot: Entity, targetRoot: Entity): any[] { + private _cloneArguments(args: any[], cloneMap: Map): any[] { if (!args || args.length === 0) return []; const len = args.length; const clonedArgs = new Array(len); for (let i = 0; i < len; i++) { + // Entity/Component args remap to their clone counterpart; everything else (primitives, + // external refs, plain objects) is not in the map and passes through unchanged. const arg = args[i]; - if (arg instanceof Entity) { - clonedArgs[i] = CloneUtils.remapEntity(srcRoot, targetRoot, arg); - } else if (arg instanceof Component) { - clonedArgs[i] = CloneUtils.remapComponent(srcRoot, targetRoot, arg); - } else { - clonedArgs[i] = arg; - } + clonedArgs[i] = cloneMap.get(arg) ?? arg; } return clonedArgs; } diff --git a/packages/core/src/Transform.ts b/packages/core/src/Transform.ts index f822474dcc..da8abbf4bb 100644 --- a/packages/core/src/Transform.ts +++ b/packages/core/src/Transform.ts @@ -2,7 +2,7 @@ import { MathUtil, Matrix, Matrix3x3, Quaternion, Vector3 } from "@galacean/engi import { BoolUpdateFlag } from "./BoolUpdateFlag"; import { Component } from "./Component"; import { Entity } from "./Entity"; -import { assignmentClone, ignoreClone } from "./clone/CloneManager"; +import { property } from "./clone/CloneManager"; /** * Used to implement transformation related functions. @@ -18,41 +18,25 @@ export class Transform extends Component { private static _tempMat41: Matrix = new Matrix(); private static _tempMat42: Matrix = new Matrix(); - @ignoreClone private _position: Vector3 = new Vector3(); - @ignoreClone private _rotation: Vector3 = new Vector3(); - @ignoreClone private _rotationQuaternion: Quaternion = new Quaternion(); - @ignoreClone private _scale: Vector3 = new Vector3(1, 1, 1); - @assignmentClone + @property private _localUniformScaling: boolean = true; - @ignoreClone private _worldPosition: Vector3 = new Vector3(); - @ignoreClone private _worldRotation: Vector3 = new Vector3(); - @ignoreClone private _worldRotationQuaternion: Quaternion = new Quaternion(); - @ignoreClone private _worldUniformScaling: boolean = true; - @ignoreClone private _lossyWorldScale: Vector3 = new Vector3(1, 1, 1); - @ignoreClone private _localMatrix: Matrix = new Matrix(); - @ignoreClone private _worldMatrix: Matrix = new Matrix(); - @ignoreClone private _worldForward: Vector3 = null; - @ignoreClone private _worldRight: Vector3 = null; - @ignoreClone private _worldUp: Vector3 = null; - @ignoreClone protected _isParentDirty: boolean = true; private _parentTransformCache: Transform = null; - @ignoreClone private _dirtyFlag: number = TransformModifyFlags.LqLmWmWpWeWqWsWus; /** @@ -651,13 +635,11 @@ export class Transform extends Component { return parentCache; } - @ignoreClone protected _onPositionChanged(): void { this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix); this._updateWorldPositionFlag(); } - @ignoreClone protected _onWorldPositionChanged(): void { const worldPosition = this._worldPosition; const parent = this._getParentTransform(); @@ -813,14 +795,12 @@ export class Transform extends Component { this._rotateByQuat(rotQuat, relativeToLocal); } - @ignoreClone private _onRotationChanged(): void { this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix | TransformModifyFlags.LocalQuat); this._setDirtyFlagFalse(TransformModifyFlags.LocalEuler); this._updateWorldRotationFlag(); } - @ignoreClone private _onWorldRotationChanged(): void { const worldRotation = this._worldRotation; Quaternion.rotationEuler( @@ -832,14 +812,12 @@ export class Transform extends Component { this._setDirtyFlagFalse(TransformModifyFlags.WorldEuler); } - @ignoreClone private _onRotationQuaternionChanged(): void { this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix | TransformModifyFlags.LocalEuler); this._setDirtyFlagFalse(TransformModifyFlags.LocalQuat); this._updateWorldRotationFlag(); } - @ignoreClone private _onWorldRotationQuaternionChanged(): void { const worldRotationQuaternion = this._worldRotationQuaternion; const parent = this._getParentTransform(); @@ -853,7 +831,6 @@ export class Transform extends Component { this._setDirtyFlagFalse(TransformModifyFlags.WorldQuat); } - @ignoreClone private _onScaleChanged(): void { const { x, y, z } = this._scale; this._setDirtyFlagTrue(TransformModifyFlags.LocalMatrix); diff --git a/packages/core/src/VirtualCamera.ts b/packages/core/src/VirtualCamera.ts index 4c8598888e..77faaaa8ac 100644 --- a/packages/core/src/VirtualCamera.ts +++ b/packages/core/src/VirtualCamera.ts @@ -1,22 +1,26 @@ import { Matrix, Vector3 } from "@galacean/engine-math"; -import { ignoreClone } from "./clone/CloneManager"; +import { property } from "./clone/CloneManager"; /** * @internal */ export class VirtualCamera { + @property isOrthographic: boolean = false; + @property nearClipPlane: number = 0.1; + @property farClipPlane: number = 100; - @ignoreClone position: Vector3 = new Vector3(); - @ignoreClone + // The matrices clone together with Camera's `_isCustomViewMatrix` / `_isCustomProjectionMatrix` + // flags: when a flag is true the getter short-circuits to the stored matrix and never recomputes, + // so a clone carrying the flag without the matrix would render with an identity matrix forever. + // For non-custom cameras the cloned values are recomputed on first use (dirty flags start true). + @property viewMatrix: Matrix = new Matrix(); - @ignoreClone + @property projectionMatrix: Matrix = new Matrix(); - @ignoreClone viewProjectionMatrix: Matrix = new Matrix(); /** Only orth mode use. */ - @ignoreClone forward: Vector3 = new Vector3(); } diff --git a/packages/core/src/animation/Animator.ts b/packages/core/src/animation/Animator.ts index 42fc2ee3fa..b6b53a55e5 100644 --- a/packages/core/src/animation/Animator.ts +++ b/packages/core/src/animation/Animator.ts @@ -5,7 +5,7 @@ import { Entity } from "../Entity"; import { Renderer } from "../Renderer"; import { Script } from "../Script"; import { Logger } from "../base/Logger"; -import { assignmentClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { AnimatorController } from "./AnimatorController"; import { AnimatorControllerLayer } from "./AnimatorControllerLayer"; import { AnimatorControllerParameter, AnimatorControllerParameterValue } from "./AnimatorControllerParameter"; @@ -34,9 +34,10 @@ export class Animator extends Component { private static _tempScripts: Script[] = []; /** Culling mode of this Animator. */ + @property cullingMode: AnimatorCullingMode = AnimatorCullingMode.None; /** The playback speed of the Animator, 1.0 is normal playback speed. */ - @assignmentClone + @property speed = 1.0; /** @internal */ @@ -44,24 +45,17 @@ export class Animator extends Component { /** @internal */ _onUpdateIndex = -1; - @assignmentClone + @property protected _animatorController: AnimatorController; - @ignoreClone protected _controllerUpdateFlag: BoolUpdateFlag; - @ignoreClone protected _updateMark = 0; - @ignoreClone private _animatorLayersData = new Array(); - @ignoreClone private _curveOwnerPool: Record>> = Object.create(null); - @ignoreClone private _parametersValueMap = >Object.create(null); - @ignoreClone private _tempAnimatorStateInfo: IAnimatorStateInfo = { layerIndex: -1, state: null }; - @ignoreClone private _controlledRenderers = new Array(); /** @@ -346,7 +340,8 @@ export class Animator extends Component { _cloneTo(target: Animator): void { const animatorController = target._animatorController; if (animatorController) { - target._addResourceReferCount(animatorController, 1); + // Ref count is bumped centrally by the clone gate (Assignment branch); only the change flag + // needs re-registering here. target._controllerUpdateFlag = animatorController._registerChangeFlag(); } } diff --git a/packages/core/src/asset/ReferResource.ts b/packages/core/src/asset/ReferResource.ts index 1460d048e8..c1f9a1b9f5 100644 --- a/packages/core/src/asset/ReferResource.ts +++ b/packages/core/src/asset/ReferResource.ts @@ -1,10 +1,13 @@ import { EngineObject } from "../base/EngineObject"; import { Engine } from "../Engine"; import { IReferable } from "./IReferable"; +import { CloneMode } from "../clone/enums/CloneMode"; +import { defaultCloneMode } from "../clone/CloneManager"; /** * The base class of assets, with reference counting capability. */ +@defaultCloneMode(CloneMode.Assignment) export abstract class ReferResource extends EngineObject implements IReferable { /** Whether to ignore the garbage collection check, if it is true, it will not be affected by ResourceManager.gc(). */ isGCIgnored: boolean = false; diff --git a/packages/core/src/audio/AudioSource.ts b/packages/core/src/audio/AudioSource.ts index 29a3ed8bc1..0bc8a47d33 100644 --- a/packages/core/src/audio/AudioSource.ts +++ b/packages/core/src/audio/AudioSource.ts @@ -1,4 +1,4 @@ -import { assignmentClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Component } from "../Component"; import { Entity } from "../Entity"; import { AudioClip } from "./AudioClip"; @@ -9,32 +9,27 @@ import { AudioManager } from "./AudioManager"; */ export class AudioSource extends Component { /** If set to true, the audio component automatically begins to play on startup. */ + @property playOnEnabled = true; - @ignoreClone private _isPlaying = false; - @ignoreClone private _pendingPlay = false; - @assignmentClone + @property private _clip: AudioClip; - @ignoreClone private _gainNode: GainNode; - @ignoreClone private _sourceNode: AudioBufferSourceNode | null = null; - @ignoreClone private _pausedTime = -1; - @ignoreClone private _playTime = -1; - @assignmentClone + @property private _volume = 1; - @assignmentClone + @property private _lastVolume = 1; - @assignmentClone + @property private _playbackRate = 1; - @assignmentClone + @property private _loop = false; /** @@ -218,7 +213,6 @@ export class AudioSource extends Component { * @internal */ _cloneTo(target: AudioSource): void { - target._clip?._addReferCount(1); target._gainNode.gain.setValueAtTime(target._volume, AudioManager.getContext().currentTime); } @@ -245,7 +239,6 @@ export class AudioSource extends Component { this.clip = null; } - @ignoreClone private _onPlayEnd(): void { this.stop(); } diff --git a/packages/core/src/base/EngineObject.ts b/packages/core/src/base/EngineObject.ts index 4354f3d61b..ed3343aaec 100644 --- a/packages/core/src/base/EngineObject.ts +++ b/packages/core/src/base/EngineObject.ts @@ -1,4 +1,3 @@ -import { ignoreClone } from "../clone/CloneManager"; import { Engine } from "../Engine"; /** @@ -7,10 +6,8 @@ import { Engine } from "../Engine"; export class EngineObject { private static _instanceIdCounter = 0; - @ignoreClone readonly instanceId = ++EngineObject._instanceIdCounter; /** @internal */ - @ignoreClone _engine: Engine; /** @internal */ _pendingDestroy = false; diff --git a/packages/core/src/clone/CloneManager.ts b/packages/core/src/clone/CloneManager.ts index be46462982..2e1d29d999 100644 --- a/packages/core/src/clone/CloneManager.ts +++ b/packages/core/src/clone/CloneManager.ts @@ -1,198 +1,238 @@ -import { Entity } from "../Entity"; import { TypedArray } from "../base/Constant"; +import { Logger } from "../base/Logger"; import { ICustomClone } from "./ComponentCloner"; import { CloneMode } from "./enums/CloneMode"; /** - * Property decorator, ignore the property when cloning. - */ -export function ignoreClone(target: Object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Ignore); -} - -/** - * Property decorator, assign value to the property when cloning. + * Property decorator. Marks a field as a managed property of the type — it participates in + * cloning. HOW it is cloned is decided by the field's runtime type + the type's `@defaultCloneMode` + * (Entity / Component → Remap, ReferResource / interned flyweights → Assignment, otherwise → deep + * clone). Opt-in: an unmarked field is not cloned. * - * @remarks - * If it's a primitive type, the value will be copied. - * If it's a class type, the reference will be copied. + * Contracts a `@property` field imposes on the types it can hold: + * - A deep-cloned class (no `@defaultCloneMode`) must be parameterless-constructible — the + * Construct stage builds an empty shell with `new ctor()` (no reuse exists for container + * members); state is restored by the Populate stage / `_cloneTo`. + * - A constructor that presets a ref-counted resource into a `@property` slot must own a + * reference for it (assign via its setter or an explicit +1): the clone gate releases the + * slot's old value when overwriting it with the source's. */ -export function assignmentClone(target: Object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Assignment); +export function property(target: Object, propertyKey: string): void { + let fields = CloneManager._subPropertyMap.get(target.constructor); + if (!fields) { + fields = new Set(); + CloneManager._subPropertyMap.set(target.constructor, fields); + } + fields.add(propertyKey); + // A new registration extends this class AND every subclass; the affected subclass set is + // unknown here, so drop all flattened sets (registration is a cold path, rebuild is lazy). + CloneManager._propertyMap.clear(); } /** - * Property decorator, shallow clone the property when cloning. - * After cloning, it will keep its own reference independent, and use the method of assignment to clone all its internal properties. - * if the internal property is a primitive type, the value will be copied, if the internal property is a reference type, its reference address will be copied.。 + * Class decorator that sets the default clone mode for instances of the decorated type — applied when + * an undecorated field holds such an instance. Built-in defaults: Entity / Component → `Remap`, + * ReferResource → `Assignment`. Use it on a custom type that should be shared (not deep cloned) when + * it appears inside a cloned object. * - * @remarks - * Applicable to Object, Array, TypedArray and Class types. - */ -export function shallowClone(target: Object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Shallow); -} - -/** - * Property decorator, deep clone the property when cloning. - * After cloning, it will maintain its own reference independence, and all its internal deep properties will remain completely independent. + * @param mode - The clone mode applied to instances of the decorated type * - * @remarks - * Applicable to Object, Array, TypedArray and Class types. - * If Class is encountered during the deep cloning process, the custom cloning function of the object will be called first. - * Custom cloning requires the object to implement the IClone interface. + * @example + * ```ts + * @defaultCloneMode(CloneMode.Assignment) + * class MySharedAsset {} + * ``` */ -export function deepClone(target: Object, propertyKey: string): void { - CloneManager.registerCloneMode(target, propertyKey, CloneMode.Deep); +export function defaultCloneMode(mode: CloneMode) { + return function (target: Function): void { + Object.defineProperty(target.prototype, "_defaultCloneMode", { value: mode }); + }; } /** * @internal * Clone manager. + * + * Opt-in model: a class instance clones only the fields marked `@property`; HOW each is cloned is + * decided by the field's runtime type, not per field: + * - primitive / null / undefined → assign by value. + * - function → skipped (transient; the clone's constructor re-establishes bound handlers). + * - object → the type's default clone mode (`@defaultCloneMode`): + * Remap (Entity / Component) → resolve to the clone via the identity map (`cloneMap.get(x) ?? x`); + * refs outside the cloned subtree are absent and kept as-is. + * Assignment (ReferResource assets + interned flyweights) → share the reference; a ref-counted + * resource gets +1 (balanced by the clone's own destroy). + * no default → deep clone. + * + * Deep clone: + * - Containers: TypedArray → buffer copy; Array / Map / Set → new container, each member cloned + * through the gate (deep / remap / share). + * - Plain object (`constructor === Object`) → clone every entry (data dictionary). + * - Class instance — 3-stage lifecycle: + * 1. Construct — reuse `target[k]` if it already holds an independent same-type instance, + * else `new ctor()`. A cloneable class must be parameterless-constructible (do real init + * in `_cloneTo` / lifecycle, not the constructor). + * 2. Populate — `copyFrom` (value-type fast path) OR recurse its own `@property` fields. + * 3. Finalize — `_cloneTo` post-clone hook, ALWAYS runs after populate (native sync, refcount, + * update-flag, derived state, cache rebuild). Equivalent to Unreal's PostDuplicate. + * + * Cycles / shared sub-graphs dedup through the same identity map. */ export class CloneManager { - /** @internal */ - static _subCloneModeMap = new Map(); - /** @internal */ - static _cloneModeMap = new Map(); + /** @internal Own `@property` field names per class (excluding inherited). */ + static _subPropertyMap = new Map>(); + /** @internal Flattened `@property` field names per class (across the prototype chain), cached. */ + static _propertyMap = new Map>(); private static _objectType = Object.getPrototypeOf(Object); /** - * Register clone mode. - * @param target - Clone target - * @param propertyKey - Clone property name - * @param mode - Clone mode - */ - static registerCloneMode(target: Object, propertyKey: string, mode: CloneMode): void { - let targetMap = CloneManager._subCloneModeMap.get(target.constructor); - if (!targetMap) { - targetMap = Object.create(null); - CloneManager._subCloneModeMap.set(target.constructor, targetMap); - } - targetMap[propertyKey] = mode; - } - - /** - * Get the clone mode according to the prototype chain. + * Get the property field names of a type, flattened across its prototype chain. */ - static getCloneMode(type: Function): Object { - let cloneModes = CloneManager._cloneModeMap.get(type); - if (!cloneModes) { - cloneModes = Object.create(null); - CloneManager._cloneModeMap.set(type, cloneModes); + static getProperties(type: Function): Set { + let fields = CloneManager._propertyMap.get(type); + if (!fields) { + fields = new Set(); + CloneManager._propertyMap.set(type, fields); const objectType = CloneManager._objectType; - const cloneModeMap = CloneManager._subCloneModeMap; + const subMap = CloneManager._subPropertyMap; while (type !== objectType) { - const subCloneModes = cloneModeMap.get(type); - if (subCloneModes) { - Object.assign(cloneModes, subCloneModes); + const own = subMap.get(type); + if (own) { + own.forEach((field) => fields.add(field)); } type = Object.getPrototypeOf(type); } } - return cloneModes; + return fields; } - static cloneProperty( - source: Object, - target: Object, - k: string | number, - cloneMode: CloneMode, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { - const sourceProperty = source[k]; + static copyProperty(source: Object, target: Object, k: string | number, cloneMap: Map): void { + target[k] = CloneManager._cloneValue(source[k], target[k], cloneMap); + } - // Remappable references (Entity/Component) are always remapped, regardless of clone decorator - if (sourceProperty instanceof Object && (sourceProperty)._remap) { - target[k] = (sourceProperty)._remap(srcRoot, targetRoot); - return; + /** + * Decide how to clone one value (the gate — does no recursion itself): + * - primitive / null / undefined → by value. + * - function → keep the clone's own (transient; re-established by its constructor). + * - Assignment → share the source reference. For a ref-counted resource, release the clone's reused + * slot value (its constructor-preset default) and acquire the source — net-balanced by the clone's + * own destroy; no-op for non-ref-counted flyweights. + * - Remap → resolve an Entity / Component to its clone via the identity map; refs outside the + * cloned subtree are kept as-is. + * - otherwise → deep clone. + * + * `reuse` is the clone's existing slot value, reused in place for value types / typed arrays so any + * constructor-bound callbacks survive; pass `undefined` when there is no slot. + */ + private static _cloneValue(value: any, reuse: any, cloneMap: Map): any { + if (!(value instanceof Object)) return value; + if (typeof value === "function") return reuse; + + const cloneMode = (value)._defaultCloneMode ?? CloneMode.Deep; + if (cloneMode === CloneMode.Assignment) { + const reusedResource = <{ _addReferCount?(count: number): void; refCount?: number }>reuse; + if (reusedResource?._addReferCount) { + // The slot's old value must own one reference (see the `@property` contract): a constructor + // preset that skipped the +1 would get its count corrupted here and be released early. + const presetRefCount = reusedResource.refCount; + presetRefCount !== undefined && + presetRefCount <= 0 && + Logger.error( + `CloneManager: the clone's preset ${reuse.constructor.name} holds no owned reference; ` + + `a constructor presetting a ref-counted resource must acquire it (assign via its setter or an explicit +1).` + ); + reusedResource._addReferCount(-1); + } + (<{ _addReferCount?(count: number): void }>value)._addReferCount?.(1); + return value; + } + if (cloneMode === CloneMode.Remap) { + return cloneMap.get(value) ?? value; } + return CloneManager._deepClone(value, reuse, cloneMap); + } - if (cloneMode === CloneMode.Ignore) return; + /** + * Deep-clone one object graph. Cycles / shared sub-graphs dedup through the identity map; every + * member recurses back through {@link _cloneValue}. + */ + private static _deepClone(value: any, reuse: any, cloneMap: Map): any { + const existing = cloneMap.get(value); + if (existing) return existing; - // Primitives, undecorated, or @assignmentClone: direct assign - if (!(sourceProperty instanceof Object) || cloneMode === undefined || cloneMode === CloneMode.Assignment) { - target[k] = sourceProperty; - return; + // Value type (Vector3, Color, Matrix, ...) — copy in place into the reused instance so its + // constructor-bound callbacks survive. + if ((value).copyFrom) { + const dst = + reuse && reuse !== value && reuse.constructor === value.constructor ? reuse : new (value.constructor)(); + cloneMap.set(value, dst); + (dst).copyFrom(value); + (value)._cloneTo?.(dst, cloneMap); + return dst; } - // @shallowClone / @deepClone: deep copy complex objects - const type = sourceProperty.constructor; - switch (type) { - case Uint8Array: - case Uint16Array: - case Uint32Array: - case Int8Array: - case Int16Array: - case Int32Array: - case Float32Array: - case Float64Array: - let targetPropertyT = target[k]; - if (targetPropertyT == null || targetPropertyT.length !== (sourceProperty).length) { - target[k] = (sourceProperty).slice(); - } else { - targetPropertyT.set(sourceProperty); - } - break; - case Array: - let targetPropertyA = >target[k]; - const length = (>sourceProperty).length; - if (targetPropertyA == null) { - target[k] = targetPropertyA = new Array(length); - } else { - targetPropertyA.length = length; - } - for (let i = 0; i < length; i++) { - CloneManager.cloneProperty( - >sourceProperty, - targetPropertyA, - i, - cloneMode, - srcRoot, - targetRoot, - deepInstanceMap - ); - } - break; - default: - let targetProperty = target[k]; - // If the target property is undefined, create new instance and keep reference sharing like the source - if (!targetProperty) { - targetProperty = deepInstanceMap.get(sourceProperty); - if (!targetProperty) { - targetProperty = new sourceProperty.constructor(); - deepInstanceMap.set(sourceProperty, targetProperty); - } - target[k] = targetProperty; - } + // Typed array — copy into the reused buffer when it fits, else a fresh slice. + if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { + const src = value; + if (reuse && reuse.constructor === src.constructor && (reuse).length === src.length) { + (reuse).set(src); + return reuse; + } + return src.slice(); + } - if ((sourceProperty).copyFrom) { - (targetProperty).copyFrom(sourceProperty); - } else { - const cloneModes = CloneManager.getCloneMode(sourceProperty.constructor); - for (let k in sourceProperty) { - CloneManager.cloneProperty( - sourceProperty, - targetProperty, - k, - cloneModes[k], - srcRoot, - targetRoot, - deepInstanceMap - ); - } - (sourceProperty)._cloneTo?.(targetProperty, srcRoot, targetRoot); - } - break; + // Container — always a fresh instance; every member recurses through the gate. + if (Array.isArray(value)) { + const dst = new Array(value.length); + cloneMap.set(value, dst); + for (let i = 0, n = value.length; i < n; i++) { + dst[i] = CloneManager._cloneValue(value[i], undefined, cloneMap); + } + return dst; + } + if (value instanceof Map) { + const dst = new Map(); + cloneMap.set(value, dst); + value.forEach((v, key) => { + dst.set(CloneManager._cloneValue(key, undefined, cloneMap), CloneManager._cloneValue(v, undefined, cloneMap)); + }); + return dst; + } + if (value instanceof Set) { + const dst = new Set(); + cloneMap.set(value, dst); + value.forEach((v) => dst.add(CloneManager._cloneValue(v, undefined, cloneMap))); + return dst; } + + // Object — reuse a same-type instance else `new ctor()` (must be parameterless-constructible; do + // real init in `_cloneTo` / lifecycle). A plain object clones every entry; a class clones its own + // `@property` fields. Then run the post-clone hook. + const dst = + reuse && reuse !== value && reuse.constructor === value.constructor ? reuse : new (value.constructor)(); + cloneMap.set(value, dst); + if (value.constructor === Object) { + for (const key in value) { + dst[key] = CloneManager._cloneValue(value[key], dst[key], cloneMap); + } + } else { + CloneManager.getProperties(value.constructor).forEach((key) => { + dst[key] = CloneManager._cloneValue(value[key], dst[key], cloneMap); + }); + } + (value)._cloneTo?.(dst, cloneMap); + return dst; } - static deepCloneObject(source: Object, target: Object, deepInstanceMap: Map): void { + /** + * Copy every enumerable property of `source` into the pre-existing `target`, deep-cloning each + * value through the type-driven gate (deep / share / remap). The opt-out helper for objects outside + * the `@property` system (e.g. ShaderMacroCollection). + */ + static copyProperties(source: Object, target: Object, cloneMap: Map): void { for (let k in source) { - CloneManager.cloneProperty(source, target, k, CloneMode.Deep, null, null, deepInstanceMap); + CloneManager.copyProperty(source, target, k, cloneMap); } } } diff --git a/packages/core/src/clone/CloneUtils.ts b/packages/core/src/clone/CloneUtils.ts deleted file mode 100644 index bded5ed474..0000000000 --- a/packages/core/src/clone/CloneUtils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component } from "../Component"; -import { Entity } from "../Entity"; - -/** - * @internal - * Utility functions for remapping Entity/Component references during cloning. - */ -export class CloneUtils { - private static _tempRemapPath: number[] = []; - - static remapEntity(srcRoot: Entity, targetRoot: Entity, entity: Entity): Entity { - const path = CloneUtils._tempRemapPath; - if (!CloneUtils._getEntityHierarchyPath(srcRoot, entity, path)) return entity; - return CloneUtils._getEntityByHierarchyPath(targetRoot, path); - } - - static remapComponent(srcRoot: Entity, targetRoot: Entity, component: T): T { - const path = CloneUtils._tempRemapPath; - const srcEntity = component.entity; - if (!CloneUtils._getEntityHierarchyPath(srcRoot, srcEntity, path)) return component; - return CloneUtils._getEntityByHierarchyPath(targetRoot, path)._components[ - srcEntity._components.indexOf(component) - ] as T; - } - - private static _getEntityHierarchyPath(rootEntity: Entity, searchEntity: Entity, inversePath: number[]): boolean { - inversePath.length = 0; - while (searchEntity !== rootEntity) { - const parent = searchEntity.parent; - if (!parent) { - return false; - } - inversePath.push(searchEntity.siblingIndex); - searchEntity = parent; - } - return true; - } - - private static _getEntityByHierarchyPath(rootEntity: Entity, inversePath: number[]): Entity { - let entity = rootEntity; - for (let i = inversePath.length - 1; i >= 0; i--) { - entity = entity.children[inversePath[i]]; - } - return entity; - } -} diff --git a/packages/core/src/clone/ComponentCloner.ts b/packages/core/src/clone/ComponentCloner.ts index f4e1b11651..851f437e50 100644 --- a/packages/core/src/clone/ComponentCloner.ts +++ b/packages/core/src/clone/ComponentCloner.ts @@ -1,6 +1,6 @@ import { Component } from "../Component"; -import { Entity } from "../Entity"; import { CloneManager } from "./CloneManager"; +import { CloneMode } from "./enums/CloneMode"; /** * Custom clone interface. @@ -8,12 +8,14 @@ import { CloneManager } from "./CloneManager"; export interface ICustomClone { /** * @internal + * Default clone mode for instances of this type, applied when a field holding the instance is + * undecorated (set via `@defaultCloneMode`). Absence falls back to deep clone. */ - _remap?(srcRoot: Entity, targetRoot: Entity): Object; + readonly _defaultCloneMode?: CloneMode; /** * @internal */ - _cloneTo?(target: ICustomClone, srcRoot?: Entity, targetRoot?: Entity): void; + _cloneTo?(target: ICustomClone, cloneMap?: Map): void; /** * @internal */ @@ -26,17 +28,10 @@ export class ComponentCloner { * @param source - Clone source * @param target - Clone target */ - static cloneComponent( - source: Component, - target: Component, - srcRoot: Entity, - targetRoot: Entity, - deepInstanceMap: Map - ): void { - const cloneModes = CloneManager.getCloneMode(source.constructor); - for (let k in source) { - CloneManager.cloneProperty(source, target, k, cloneModes[k], srcRoot, targetRoot, deepInstanceMap); - } - ((source as unknown))._cloneTo?.(target, srcRoot, targetRoot); + static cloneComponent(source: Component, target: Component, cloneMap: Map): void { + CloneManager.getProperties(source.constructor).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + ((source as unknown))._cloneTo?.(target, cloneMap); } } diff --git a/packages/core/src/clone/enums/CloneMode.ts b/packages/core/src/clone/enums/CloneMode.ts index 77158ce3ce..fad96339b5 100644 --- a/packages/core/src/clone/enums/CloneMode.ts +++ b/packages/core/src/clone/enums/CloneMode.ts @@ -1,13 +1,11 @@ /** - * Clone mode. + * How a value is cloned, decided by its type (see `@defaultCloneMode`). */ export enum CloneMode { - /** Ignore clone. */ - Ignore, - /** Assignment clone. */ + /** Share the reference; a ref-counted resource is kept alive by the clone (used for assets). */ Assignment, - /** Shallow clone. */ - Shallow, - /** Deep clone. */ - Deep + /** Recursively deep clone the value. */ + Deep, + /** Remap an Entity / Component reference to its clone within the cloned subtree, else keep as-is. */ + Remap } diff --git a/packages/core/src/env-probe/Probe.ts b/packages/core/src/env-probe/Probe.ts index 2837a503de..73c8c7eba9 100644 --- a/packages/core/src/env-probe/Probe.ts +++ b/packages/core/src/env-probe/Probe.ts @@ -1,4 +1,5 @@ import { Camera } from "../Camera"; +import { property } from "../clone/CloneManager"; import { Layer } from "../Layer"; import { Script } from "../Script"; import { RenderBufferDepthFormat, Texture, Texture2D, TextureCube } from "../texture"; @@ -19,21 +20,25 @@ export abstract class Probe extends Script { /** * Probe's layer, render everything by default. */ + @property probeLayer: Layer = Layer.Everything; /** * The width of the probe rendering target. */ + @property width: number = 1024; /** * The height of the probe rendering target. */ + @property height: number = 1024; /** * When using WebGL2, you can turn on MSAA at the hardware layer. */ + @property antiAliasing: number = 1; /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be483eccbd..308c4751be 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -70,7 +70,7 @@ export * from "./env-probe/index"; export * from "./shader/index"; export * from "./Layer"; export * from "./clone/CloneManager"; -export { CloneUtils } from "./clone/CloneUtils"; +export * from "./clone/enums/CloneMode"; export * from "./renderingHardwareInterface/index"; export * from "./physics/index"; export * from "./Utils"; diff --git a/packages/core/src/lighting/DirectLight.ts b/packages/core/src/lighting/DirectLight.ts index 2184474d75..3fb60e6031 100644 --- a/packages/core/src/lighting/DirectLight.ts +++ b/packages/core/src/lighting/DirectLight.ts @@ -1,5 +1,5 @@ import { Vector3 } from "@galacean/engine-math"; -import { ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { ShaderData } from "../shader"; import { ShaderProperty } from "../shader/ShaderProperty"; import { Light } from "./Light"; @@ -25,9 +25,9 @@ export class DirectLight extends Light { * The offset distance in the opposite direction of light direction when generating shadows. * @remarks Increasing this value can avoid the holes in the shadow caused by low polygon models. */ + @property shadowNearPlaneOffset = 0.1; - @ignoreClone private _reverseDirection: Vector3 = new Vector3(); /** diff --git a/packages/core/src/lighting/Light.ts b/packages/core/src/lighting/Light.ts index 5406de6243..88318dd1a6 100644 --- a/packages/core/src/lighting/Light.ts +++ b/packages/core/src/lighting/Light.ts @@ -1,7 +1,7 @@ import { Color, MathUtil, Matrix } from "@galacean/engine-math"; import { Component } from "../Component"; import { Layer } from "../Layer"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { ShadowType } from "../shadow"; /** @@ -12,31 +12,34 @@ export abstract class Light extends Component { * Culling mask - which layers the light affect. * @remarks Support bit manipulation, corresponding to `Layer`. */ + @property cullingMask = Layer.Everything; /** How this light casts shadows. */ + @property shadowType = ShadowType.None; /** Shadow bias.*/ + @property shadowBias = 1; /** Shadow mapping normal-based bias. */ + @property shadowNormalBias = 1; /** * @deprecated * Please use `shadowNearPlaneOffset` instead. */ + @property shadowNearPlane = 0.1; /** @internal */ - @ignoreClone _lightIndex = -1; + @property private _shadowStrength = 1.0; - @deepClone + @property private _color = new Color(1, 1, 1, 1); - @ignoreClone private _viewMat: Matrix; - @ignoreClone private _inverseViewMat: Matrix; /** Shadow intensity, the larger the value, the clearer and darker the shadow, range [0,1]. */ diff --git a/packages/core/src/lighting/PointLight.ts b/packages/core/src/lighting/PointLight.ts index 8e5b18973b..f708d26c87 100644 --- a/packages/core/src/lighting/PointLight.ts +++ b/packages/core/src/lighting/PointLight.ts @@ -1,4 +1,5 @@ import { Vector3 } from "@galacean/engine-math"; +import { property } from "../clone/CloneManager"; import { ShaderData } from "../shader"; import { ShaderProperty } from "../shader/ShaderProperty"; import { Light } from "./Light"; @@ -23,6 +24,7 @@ export class PointLight extends Light { } /** Defines a distance cutoff at which the light's intensity must be considered zero. */ + @property distance: number = 100; /** diff --git a/packages/core/src/lighting/SpotLight.ts b/packages/core/src/lighting/SpotLight.ts index 26dd61545b..c7680c60b0 100644 --- a/packages/core/src/lighting/SpotLight.ts +++ b/packages/core/src/lighting/SpotLight.ts @@ -2,7 +2,7 @@ import { Matrix, Vector3 } from "@galacean/engine-math"; import { ShaderData } from "../shader"; import { ShaderProperty } from "../shader/ShaderProperty"; import { Light } from "./Light"; -import { ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; /** * Spot light. @@ -30,13 +30,15 @@ export class SpotLight extends Light { } /** Defines a distance cutoff at which the light's intensity must be considered zero. */ + @property distance: number = 100; /** Angle, in radians, from centre of spotlight where falloff begins. */ + @property angle: number = Math.PI / 6; /** Angle, in radians, from falloff begins to ends. */ + @property penumbra: number = Math.PI / 12; - @ignoreClone private _inverseDirection: Vector3 = new Vector3(); /** diff --git a/packages/core/src/mesh/MeshRenderer.ts b/packages/core/src/mesh/MeshRenderer.ts index 127eec663c..ba6c7c54f9 100644 --- a/packages/core/src/mesh/MeshRenderer.ts +++ b/packages/core/src/mesh/MeshRenderer.ts @@ -4,7 +4,7 @@ import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; import { Renderer, RendererUpdateFlags } from "../Renderer"; import { Logger } from "../base/Logger"; -import { ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Mesh, MeshModifyFlags } from "../graphic/Mesh"; import { ShaderMacro } from "../shader/ShaderMacro"; @@ -20,10 +20,10 @@ export class MeshRenderer extends Renderer { private static _normalMacro = ShaderMacro.getByName("RENDERER_HAS_NORMAL"); private static _tangentMacro = ShaderMacro.getByName("RENDERER_HAS_TANGENT"); + @property private _enableVertexColor: boolean = false; /** @internal */ - @ignoreClone _mesh: Mesh; /** @@ -216,7 +216,6 @@ export class MeshRenderer extends Renderer { this._mesh = mesh; } - @ignoreClone private _onMeshChanged(type: MeshModifyFlags): void { type & MeshModifyFlags.Bounds && (this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume); type & MeshModifyFlags.VertexElements && (this._dirtyUpdateFlag |= MeshRendererUpdateFlags.VertexElementMacro); diff --git a/packages/core/src/mesh/Skin.ts b/packages/core/src/mesh/Skin.ts index 18489805c5..73a3a64ad5 100644 --- a/packages/core/src/mesh/Skin.ts +++ b/packages/core/src/mesh/Skin.ts @@ -3,7 +3,7 @@ import { Entity } from "../Entity"; import { UpdateFlagManager } from "../UpdateFlagManager"; import { Utils } from "../Utils"; import { EngineObject } from "../base/EngineObject"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { SkinnedMeshRenderer } from "./SkinnedMeshRenderer"; /** @@ -11,20 +11,19 @@ import { SkinnedMeshRenderer } from "./SkinnedMeshRenderer"; */ export class Skin extends EngineObject { /** Inverse bind matrices. */ - @deepClone + @property inverseBindMatrices = new Array(); /** @internal */ - @deepClone + @property _skinMatrices: Float32Array; /** @internal */ - @ignoreClone _updatedManager = new UpdateFlagManager(); + @property private _rootBone: Entity; - @deepClone + @property private _bones = new Array(); - @ignoreClone private _updateMark = -1; /** @@ -64,8 +63,12 @@ export class Skin extends EngineObject { } } - constructor(public name: string) { + @property + name: string; + + constructor(name: string = "") { super(null); + this.name = name; } /** @@ -93,6 +96,7 @@ export class Skin extends EngineObject { } /** @deprecated Please use `bones` instead. */ + @property public joints: string[] = []; /** @deprecated Please use `rootBone` instead. */ diff --git a/packages/core/src/mesh/SkinnedMeshRenderer.ts b/packages/core/src/mesh/SkinnedMeshRenderer.ts index 4d7f726184..a58b534e72 100644 --- a/packages/core/src/mesh/SkinnedMeshRenderer.ts +++ b/packages/core/src/mesh/SkinnedMeshRenderer.ts @@ -4,7 +4,7 @@ import { RenderContext } from "../RenderPipeline/RenderContext"; import { RenderElement } from "../RenderPipeline/RenderElement"; import { RendererUpdateFlags } from "../Renderer"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { ShaderProperty } from "../shader"; import { Texture2D } from "../texture/Texture2D"; import { TextureFilterMode } from "../texture/enums/TextureFilterMode"; @@ -26,23 +26,18 @@ export class SkinnedMeshRenderer extends MeshRenderer { private static _jointMatrixProperty = ShaderProperty.getByName("renderer_JointMatrix"); /** @internal */ - @ignoreClone _condensedBlendShapeWeights: Float32Array; - @deepClone + @property private _localBounds: BoundingBox = new BoundingBox(); - @ignoreClone private _jointDataCreateCache: Vector2 = new Vector2(-1, -1); - @ignoreClone private _blendShapeWeights: Float32Array; - @ignoreClone private _maxVertexUniformVectors: number; - @ignoreClone private _jointTexture: Texture2D; - @deepClone + @property private _skin: Skin; /** @@ -250,12 +245,10 @@ export class SkinnedMeshRenderer extends MeshRenderer { } } - @ignoreClone private _onLocalBoundsChanged(): void { this._dirtyUpdateFlag |= RendererUpdateFlags.WorldVolume; } - @ignoreClone private _onSkinUpdated(type: SkinUpdateFlag, value: Object): void { switch (type) { case SkinUpdateFlag.BoneCountChanged: diff --git a/packages/core/src/particle/ParticleGenerator.ts b/packages/core/src/particle/ParticleGenerator.ts index ff72a1292c..4158df977d 100644 --- a/packages/core/src/particle/ParticleGenerator.ts +++ b/packages/core/src/particle/ParticleGenerator.ts @@ -1,6 +1,6 @@ import { BoundingBox, Color, Matrix, Quaternion, Vector2, Vector3 } from "@galacean/engine-math"; import { Transform } from "../Transform"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Primitive } from "../graphic/Primitive"; import { SubMesh } from "../graphic/SubMesh"; import { SubPrimitive } from "../graphic/SubPrimitive"; @@ -56,103 +56,83 @@ export class ParticleGenerator { private static readonly _transformFeedbackMacro = ShaderMacro.getByName("RENDERER_TRANSFORM_FEEDBACK"); /** Use auto random seed. */ + @property useAutoRandomSeed = true; /** Main module. */ - @deepClone + @property readonly main: MainModule; /** Emission module. */ - @deepClone + @property readonly emission = new EmissionModule(this); /** Velocity over lifetime module. */ - @deepClone + @property readonly velocityOverLifetime: VelocityOverLifetimeModule; /** Force over lifetime module. */ - @deepClone + @property readonly forceOverLifetime: ForceOverLifetimeModule; /** Limit velocity over lifetime module. */ - @deepClone + @property readonly limitVelocityOverLifetime: LimitVelocityOverLifetimeModule; /** Size over lifetime module. */ - @deepClone + @property readonly sizeOverLifetime: SizeOverLifetimeModule; /** Rotation over lifetime module. */ - @deepClone + @property readonly rotationOverLifetime = new RotationOverLifetimeModule(this); /** Color over lifetime module. */ - @deepClone + @property readonly colorOverLifetime = new ColorOverLifetimeModule(this); /** Texture sheet animation module. */ - @deepClone + @property readonly textureSheetAnimation = new TextureSheetAnimationModule(this); /** Noise module. */ - @deepClone + @property readonly noise: NoiseModule; /** Custom data module. */ - @deepClone + @property readonly customData: CustomDataModule; /** @internal */ - @ignoreClone _currentParticleCount = 0; /** @internal */ - @ignoreClone _playTime = 0; /** @internal */ - @ignoreClone _firstNewElement = 0; /** @internal */ - @ignoreClone _firstActiveElement = 0; /** @internal */ - @ignoreClone _firstFreeElement = 0; /** @internal */ - @ignoreClone _firstRetiredElement = 0; /** @internal */ - @ignoreClone _primitive: Primitive; /** @internal */ - @ignoreClone _vertexBufferBindings = new Array(); /** @internal */ - @ignoreClone _subPrimitive = new SubMesh(0, 0, MeshTopology.Triangles); /** @internal */ readonly _renderer: ParticleRenderer; /** @internal */ - @ignoreClone _feedbackSimulator: ParticleTransformFeedbackSimulator; /** @internal */ - @ignoreClone _useTransformFeedback = false; /** @internal */ - @ignoreClone private _feedbackBindingIndex = -1; - @ignoreClone private _isPlaying = false; - @ignoreClone private _instanceBufferResized = false; - @ignoreClone private _waitProcessRetiredElementCount = 0; - @ignoreClone private _instanceVertexBufferBinding: VertexBufferBinding; - @ignoreClone private _instanceVertices: Float32Array; + @property private _randomSeed = 0; - @ignoreClone private _transformedBoundsArray: Float32Array; - @ignoreClone private _transformedBoundsCount = 0; - @ignoreClone private _firstActiveTransformedBoundingBox = 0; - @ignoreClone private _firstFreeTransformedBoundingBox = 0; - @ignoreClone private _playStartDelay = 0; /** diff --git a/packages/core/src/particle/ParticleRenderer.ts b/packages/core/src/particle/ParticleRenderer.ts index 323d5d8e50..7442dcc6ef 100644 --- a/packages/core/src/particle/ParticleRenderer.ts +++ b/packages/core/src/particle/ParticleRenderer.ts @@ -5,7 +5,7 @@ import { Renderer, RendererUpdateFlags } from "../Renderer"; import { TransformModifyFlags } from "../Transform"; import { GLCapabilityType } from "../base/Constant"; import { Logger } from "../base/Logger"; -import { deepClone, ignoreClone, shallowClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { ModelMesh } from "../mesh/ModelMesh"; import { ShaderMacro } from "../shader/ShaderMacro"; import { ShaderProperty } from "../shader/ShaderProperty"; @@ -30,29 +30,28 @@ export class ParticleRenderer extends Renderer { private static readonly _currentTime = ShaderProperty.getByName("renderer_CurrentTime"); /** Particle generator. */ - @deepClone + @property readonly generator: ParticleGenerator; /** Specifies how much particles stretch depending on their velocity. */ + @property velocityScale = 0; /** How much are the particles stretched in their direction of motion, defined as the length of the particle compared to its width. */ + @property lengthScale = 2; /** The pivot of particle. */ - @shallowClone + @property pivot = new Vector3(); /** @internal */ - @ignoreClone _generatorBounds = new BoundingBox(); /** @internal */ - @ignoreClone _transformedBounds = new BoundingBox(); - @ignoreClone - private _mesh: ModelMesh; - @ignoreClone + @property private _renderMode: ParticleRenderMode = ParticleRenderMode.Billboard; - @ignoreClone private _currentRenderModeMacro: ShaderMacro; + @property + private _mesh: ModelMesh; private _supportInstancedArrays: boolean; /** @@ -300,7 +299,6 @@ export class ParticleRenderer extends Renderer { /** * @internal */ - @ignoreClone _onGeneratorParamsChanged(): void { this._dirtyUpdateFlag |= ParticleUpdateFlags.GeneratorVolume | ParticleUpdateFlags.TransformVolume | RendererUpdateFlags.WorldVolume; @@ -309,7 +307,6 @@ export class ParticleRenderer extends Renderer { /** * @internal */ - @ignoreClone override _onTransformChanged(type: TransformModifyFlags): void { this._dirtyUpdateFlag |= ParticleUpdateFlags.TransformVolume | RendererUpdateFlags.WorldVolume; } diff --git a/packages/core/src/particle/modules/Burst.ts b/packages/core/src/particle/modules/Burst.ts index a444381dc2..865669724c 100644 --- a/packages/core/src/particle/modules/Burst.ts +++ b/packages/core/src/particle/modules/Burst.ts @@ -1,15 +1,18 @@ -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; /** * A burst is a particle emission event, where a number of particles are all emitted at the same time */ export class Burst { + @property public time: number; - @deepClone + @property public count: ParticleCompositeCurve; + @property private _cycles: number; + @property private _repeatInterval: number; /** diff --git a/packages/core/src/particle/modules/ColorOverLifetimeModule.ts b/packages/core/src/particle/modules/ColorOverLifetimeModule.ts index c920fb3fb4..b382c5bd7b 100644 --- a/packages/core/src/particle/modules/ColorOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/ColorOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Color, Rand, Vector4 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -23,7 +23,7 @@ export class ColorOverLifetimeModule extends ParticleGeneratorModule { static readonly _gradientKeysCount = ShaderProperty.getByName("renderer_COLGradientKeysMaxTime"); /** Color gradient over lifetime. */ - @deepClone + @property color = new ParticleCompositeGradient( new ParticleGradient( [new GradientColorKey(0.0, new Color(1, 1, 1)), new GradientColorKey(1.0, new Color(1, 1, 1))], @@ -32,12 +32,9 @@ export class ColorOverLifetimeModule extends ParticleGeneratorModule { ); /** @internal */ - @ignoreClone _colorGradientRand = new Rand(0, ParticleRandomSubSeeds.ColorOverLifetime); - @ignoreClone private _gradientKeysCount = new Vector4(0, 0, 0, 0); // x: minColorKeysMaxTime, y: minAlphaKeysMaxTime, z: maxColorKeysMaxTime, w: maxAlphaKeysMaxTime - @ignoreClone private _colorMacro: ShaderMacro; /** diff --git a/packages/core/src/particle/modules/CustomDataModule.ts b/packages/core/src/particle/modules/CustomDataModule.ts index 4ad2fa6fc9..6721109a13 100644 --- a/packages/core/src/particle/modules/CustomDataModule.ts +++ b/packages/core/src/particle/modules/CustomDataModule.ts @@ -1,5 +1,5 @@ import { Color, Vector4 } from "@galacean/engine-math"; -import { CloneManager, ignoreClone } from "../../clone/CloneManager"; +import { CloneManager } from "../../clone/CloneManager"; import { Logger } from "../../base/Logger"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -53,14 +53,10 @@ export class CustomDataModule extends ParticleGeneratorModule { private static readonly _zeroColor = new Color(0, 0, 0, 0); private static readonly _zeroVector4 = new Vector4(0, 0, 0, 0); - @ignoreClone private _curves: Map = new Map(); - @ignoreClone private _gradients: Map = new Map(); - @ignoreClone private _curveStreams: CurveStream[] = []; - @ignoreClone private _gradientStreams: GradientStream[] = []; /** @@ -194,12 +190,12 @@ export class CustomDataModule extends ParticleGeneratorModule { const deepInstanceMap = new Map(); for (const [name, curve] of this._curves) { const clonedCurve = new ParticleCompositeCurve(0); - CloneManager.deepCloneObject(curve, clonedCurve, deepInstanceMap); + CloneManager.copyProperties(curve, clonedCurve, deepInstanceMap); target.addCurve(name, clonedCurve); } for (const [name, gradient] of this._gradients) { const clonedGradient = new ParticleCompositeGradient(new Color()); - CloneManager.deepCloneObject(gradient, clonedGradient, deepInstanceMap); + CloneManager.copyProperties(gradient, clonedGradient, deepInstanceMap); target.addGradient(name, clonedGradient); } } diff --git a/packages/core/src/particle/modules/EmissionModule.ts b/packages/core/src/particle/modules/EmissionModule.ts index a2d303a7d3..0a14aa1e09 100644 --- a/packages/core/src/particle/modules/EmissionModule.ts +++ b/packages/core/src/particle/modules/EmissionModule.ts @@ -1,5 +1,5 @@ import { MathUtil, Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; @@ -19,39 +19,32 @@ export class EmissionModule extends ParticleGeneratorModule { private static _tempEmitPosition = new Vector3(); /** The rate of particle emission. */ - @deepClone + @property rateOverTime: ParticleCompositeCurve = new ParticleCompositeCurve(10); /** The rate at which the emitter spawns new particles over distance. */ - @deepClone + @property rateOverDistance: ParticleCompositeCurve = new ParticleCompositeCurve(0); - @deepClone + @property _shape: BaseShape; /** @internal */ - @ignoreClone _shapeRand = new Rand(0, ParticleRandomSubSeeds.Shape); - @ignoreClone private _shapeMacro: ShaderMacro; /** @internal */ - @ignoreClone _rateRand = new Rand(0, ParticleRandomSubSeeds.EmissionRate); /** @internal */ _frameRateTime: number = 0; - @ignoreClone private _distanceAccumulator = 0; - @ignoreClone private _lastEmitPosition = new Vector3(); - @ignoreClone private _hasLastEmitPosition = false; - @deepClone + @property private _bursts: Burst[] = []; private _currentBurstIndex = 0; - @ignoreClone private _burstRand: Rand = new Rand(0, ParticleRandomSubSeeds.Burst); /** diff --git a/packages/core/src/particle/modules/ForceOverLifetimeModule.ts b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts index f96ce211c7..81b9b85489 100644 --- a/packages/core/src/particle/modules/ForceOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/ForceOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds"; @@ -27,24 +27,20 @@ export class ForceOverLifetimeModule extends ParticleGeneratorModule { static readonly _spaceProperty = ShaderProperty.getByName("renderer_FOLSpace"); /** @internal */ - @ignoreClone _forceRand = new Rand(0, ParticleRandomSubSeeds.ForceOverLifetime); - @ignoreClone private _forceMinConstant = new Vector3(); - @ignoreClone private _forceMaxConstant = new Vector3(); - @ignoreClone private _forceMacro: ShaderMacro; - @ignoreClone private _randomModeMacro: ShaderMacro; - @deepClone + @property private _forceX: ParticleCompositeCurve; - @deepClone + @property private _forceY: ParticleCompositeCurve; - @deepClone + @property private _forceZ: ParticleCompositeCurve; + @property private _space = ParticleSimulationSpace.Local; /** diff --git a/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts index 9d6a91bc54..f372ffb043 100644 --- a/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/LimitVelocityOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector2, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ShaderProperty } from "../../shader/ShaderProperty"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; @@ -42,45 +42,38 @@ export class LimitVelocityOverLifetimeModule extends ParticleGeneratorModule { static readonly _spaceProperty = ShaderProperty.getByName("renderer_LVLSpace"); /** @internal */ - @ignoreClone _speedRand = new Rand(0, ParticleRandomSubSeeds.LimitVelocityOverLifetime); - @ignoreClone private _speedMinConstantVec = new Vector3(); - @ignoreClone private _speedMaxConstantVec = new Vector3(); - @ignoreClone private _dragConstantVec = new Vector2(); - @ignoreClone private _enabledModuleMacro: ShaderMacro; - @ignoreClone private _separateAxesCachedMacro: ShaderMacro; - @ignoreClone private _speedModeMacro: ShaderMacro; - @ignoreClone private _speedRandomMacro: ShaderMacro; - @ignoreClone private _dragCurveCachedMacro: ShaderMacro; - @ignoreClone private _dragRandomCachedMacro: ShaderMacro; - @ignoreClone private _dragSizeMacro: ShaderMacro; - @ignoreClone private _dragVelocityMacro: ShaderMacro; + @property private _separateAxes = false; - @deepClone + @property private _speedX: ParticleCompositeCurve; - @deepClone + @property private _speedY: ParticleCompositeCurve; - @deepClone + @property private _speedZ: ParticleCompositeCurve; + @property private _dampen: number = 0; - @deepClone + @property private _drag: ParticleCompositeCurve; + @property private _multiplyDragByParticleSize = false; + @property private _multiplyDragByParticleVelocity = false; + @property private _space = ParticleSimulationSpace.Local; /** diff --git a/packages/core/src/particle/modules/MainModule.ts b/packages/core/src/particle/modules/MainModule.ts index d17c156d9a..c212a354b7 100644 --- a/packages/core/src/particle/modules/MainModule.ts +++ b/packages/core/src/particle/modules/MainModule.ts @@ -1,6 +1,6 @@ import { Color, Rand, Vector3, Vector4 } from "@galacean/engine-math"; import { TransformModifyFlags } from "../../Transform"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -25,79 +25,79 @@ export class MainModule implements ICustomClone { private static readonly _scaleMode = ShaderProperty.getByName("renderer_ScalingMode"); /** The duration of the Particle Generator in seconds. */ + @property duration = 5.0; /** Specifies whether the Particle Generator loops. */ + @property isLoop = true; /** Start delay in seconds. */ - @deepClone + @property startDelay = new ParticleCompositeCurve(0); /** A flag to enable 3D particle rotation, when disabled, only `startRotationZ` is used. */ + @property startRotation3D = false; /** The initial rotation of particles around the x-axis when emitted, in degrees. */ - @deepClone + @property startRotationX = new ParticleCompositeCurve(0); /** The initial rotation of particles around the y-axis when emitted, in degrees. */ - @deepClone + @property startRotationY = new ParticleCompositeCurve(0); /** The initial rotation of particles around the z-axis when emitted, in degrees. */ - @deepClone + @property startRotationZ = new ParticleCompositeCurve(0); /** Makes some particles spin in the opposite direction. */ + @property flipRotation = 0; /** The mode of start color */ - @deepClone + @property startColor = new ParticleCompositeGradient(new Color(1, 1, 1, 1)); /** A scale that this Particle Generator applies to gravity, defined by Physics.gravity. */ /** Override the default playback speed of the Particle Generator. */ + @property simulationSpeed = 1.0; /** Control how the Particle Generator applies its Transform component to the particles it emits. */ + @property scalingMode = ParticleScaleMode.Local; /** If set to true, the Particle Generator automatically begins to play on startup. */ + @property playOnEnabled = true; /** @internal */ - @ignoreClone _maxParticleBuffer = 1000; /** @internal */ - @ignoreClone readonly _startDelayRand = new Rand(0, ParticleRandomSubSeeds.StartDelay); /** @internal */ - @ignoreClone readonly _startSpeedRand = new Rand(0, ParticleRandomSubSeeds.StartSpeed); /** @internal */ - @ignoreClone readonly _startLifeTimeRand = new Rand(0, ParticleRandomSubSeeds.StartLifetime); /** @internal */ - @ignoreClone readonly _startColorRand = new Rand(0, ParticleRandomSubSeeds.StartColor); /** @internal */ - @ignoreClone readonly _startSizeRand = new Rand(0, ParticleRandomSubSeeds.StartSize); /** @internal */ - @ignoreClone readonly _startRotationRand = new Rand(0, ParticleRandomSubSeeds.StartRotation); - @ignoreClone readonly _gravityModifierRand = new Rand(0, ParticleRandomSubSeeds.GravityModifier); - @deepClone + @property private _startLifetime: ParticleCompositeCurve; - @deepClone + @property private _startSpeed: ParticleCompositeCurve; + @property private _startSize3D = false; - @deepClone + @property private _startSizeX: ParticleCompositeCurve; - @deepClone + @property private _startSizeY: ParticleCompositeCurve; - @deepClone + @property private _startSizeZ: ParticleCompositeCurve; - @deepClone + @property private _gravityModifier: ParticleCompositeCurve; + @property private _simulationSpace = ParticleSimulationSpace.Local; - @ignoreClone private _generator: ParticleGenerator; /** diff --git a/packages/core/src/particle/modules/NoiseModule.ts b/packages/core/src/particle/modules/NoiseModule.ts index 153a40b5b9..ab837ffd28 100644 --- a/packages/core/src/particle/modules/NoiseModule.ts +++ b/packages/core/src/particle/modules/NoiseModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3, Vector4 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader"; import { ParticleGenerator } from "../ParticleGenerator"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; @@ -27,37 +27,35 @@ export class NoiseModule extends ParticleGeneratorModule { static readonly _strengthMinCurveYProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinCurveY"); static readonly _strengthMinCurveZProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinCurveZ"); - @ignoreClone private _enabledModuleMacro: ShaderMacro; - @ignoreClone private _strengthCurveModeMacro: ShaderMacro; - @ignoreClone private _strengthIsRandomTwoModeMacro: ShaderMacro; - @ignoreClone private _separateAxesModeMacro: ShaderMacro; /** @internal */ - @ignoreClone _noiseRand = new Rand(0, ParticleRandomSubSeeds.Noise); - @ignoreClone private _noiseParams = new Vector4(); - @ignoreClone private _noiseOctaveParams = new Vector4(); - @ignoreClone private _strengthMinConst = new Vector3(); - @deepClone + @property private _strengthX: ParticleCompositeCurve; - @deepClone + @property private _strengthY: ParticleCompositeCurve; - @deepClone + @property private _strengthZ: ParticleCompositeCurve; + @property private _scrollSpeed = 0; + @property private _separateAxes = false; + @property private _frequency = 0.5; + @property private _octaveCount = 1; + @property private _octaveIntensityMultiplier = 0.5; + @property private _octaveFrequencyMultiplier = 2.0; /** diff --git a/packages/core/src/particle/modules/ParticleCompositeCurve.ts b/packages/core/src/particle/modules/ParticleCompositeCurve.ts index 587855ff08..31a435bffb 100644 --- a/packages/core/src/particle/modules/ParticleCompositeCurve.ts +++ b/packages/core/src/particle/modules/ParticleCompositeCurve.ts @@ -1,5 +1,5 @@ import { Vector2 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { UpdateFlagManager } from "../../UpdateFlagManager"; import { ParticleCurveMode } from "../enums/ParticleCurveMode"; import { CurveKey, ParticleCurve } from "./ParticleCurve"; @@ -8,16 +8,17 @@ import { CurveKey, ParticleCurve } from "./ParticleCurve"; * Particle composite curve. */ export class ParticleCompositeCurve { - @ignoreClone private _updateManager = new UpdateFlagManager(); + @property private _mode = ParticleCurveMode.Constant; + @property private _constantMin = 0; + @property private _constantMax = 0; - @deepClone + @property private _curveMin: ParticleCurve; - @deepClone + @property private _curveMax: ParticleCurve; - @ignoreClone private _updateDispatch: () => void; /** @@ -113,6 +114,12 @@ export class ParticleCompositeCurve { this.curveMax = value; } + /** + * Create a particle curve that generates a constant value of 0. Parameterless construction is + * required by the deep-clone Construct stage (state is then populated field-by-field). + */ + constructor(); + /** * Create a particle curve that generates a constant value. * @param constant - The constant value @@ -139,7 +146,7 @@ export class ParticleCompositeCurve { */ constructor(curveMin: ParticleCurve, curveMax: ParticleCurve); - constructor(constantOrCurve: number | ParticleCurve, constantMaxOrCurveMax?: number | ParticleCurve) { + constructor(constantOrCurve: number | ParticleCurve = 0, constantMaxOrCurveMax?: number | ParticleCurve) { this._updateDispatch = this._updateManager.dispatch.bind(this._updateManager); if (typeof constantOrCurve === "number") { if (constantMaxOrCurveMax) { diff --git a/packages/core/src/particle/modules/ParticleCompositeGradient.ts b/packages/core/src/particle/modules/ParticleCompositeGradient.ts index 59294998b4..5e498e23aa 100644 --- a/packages/core/src/particle/modules/ParticleCompositeGradient.ts +++ b/packages/core/src/particle/modules/ParticleCompositeGradient.ts @@ -1,5 +1,5 @@ import { Color } from "@galacean/engine-math"; -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ParticleGradientMode } from "../enums/ParticleGradientMode"; import { ParticleGradient } from "./ParticleGradient"; @@ -8,18 +8,19 @@ import { ParticleGradient } from "./ParticleGradient"; */ export class ParticleCompositeGradient { /** The gradient mode. */ + @property mode: ParticleGradientMode = ParticleGradientMode.Constant; /* The min constant color used by the gradient if mode is set to `TwoConstants`. */ - @deepClone + @property constantMin: Color = new Color(); /* The max constant color used by the gradient if mode is set to `TwoConstants`. */ - @deepClone + @property constantMax: Color = new Color(); /** The min gradient used by the gradient if mode is set to `Gradient`. */ - @deepClone + @property gradientMin: ParticleGradient = new ParticleGradient(); /** The max gradient used by the gradient if mode is set to `Gradient`. */ - @deepClone + @property gradientMax: ParticleGradient = new ParticleGradient(); /** @@ -44,6 +45,12 @@ export class ParticleCompositeGradient { this.gradientMax = value; } + /** + * Create a particle gradient that generates a constant white color. Parameterless construction is + * required by the deep-clone Construct stage (state is then populated field-by-field). + */ + constructor(); + /** * Create a particle gradient that generates a constant color. * @param constant - The constant color @@ -70,7 +77,10 @@ export class ParticleCompositeGradient { */ constructor(gradientMin: ParticleGradient, gradientMax: ParticleGradient); - constructor(constantOrGradient: Color | ParticleGradient, constantMaxOrGradientMax?: Color | ParticleGradient) { + constructor( + constantOrGradient: Color | ParticleGradient = new Color(), + constantMaxOrGradientMax?: Color | ParticleGradient + ) { if (constantOrGradient.constructor === Color) { if (constantMaxOrGradientMax) { this.constantMin.copyFrom(constantOrGradient); diff --git a/packages/core/src/particle/modules/ParticleCurve.ts b/packages/core/src/particle/modules/ParticleCurve.ts index fce99c6b40..d986de16c8 100644 --- a/packages/core/src/particle/modules/ParticleCurve.ts +++ b/packages/core/src/particle/modules/ParticleCurve.ts @@ -1,18 +1,15 @@ import { UpdateFlagManager } from "../../UpdateFlagManager"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; /** * Particle curve. */ export class ParticleCurve { - @ignoreClone private _updateManager = new UpdateFlagManager(); - @deepClone + @property private _keys = new Array(); - @ignoreClone private _typeArray: Float32Array; private _typeArrayDirty = false; - @ignoreClone private _updateDispatch: () => void; /** @@ -163,9 +160,10 @@ export class ParticleCurve { * The key of the curve. */ export class CurveKey { - @ignoreClone private _updateManager = new UpdateFlagManager(); + @property private _time: number; + @property private _value: number; /** diff --git a/packages/core/src/particle/modules/ParticleGeneratorModule.ts b/packages/core/src/particle/modules/ParticleGeneratorModule.ts index e9a341cabc..5d726994bd 100644 --- a/packages/core/src/particle/modules/ParticleGeneratorModule.ts +++ b/packages/core/src/particle/modules/ParticleGeneratorModule.ts @@ -1,4 +1,4 @@ -import { ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData, ShaderMacro } from "../../shader"; import { ParticleGenerator } from "../ParticleGenerator"; import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; @@ -7,9 +7,9 @@ import { ParticleCompositeCurve } from "./ParticleCompositeCurve"; * Particle generator module. */ export abstract class ParticleGeneratorModule { - @ignoreClone protected _generator: ParticleGenerator; + @property protected _enabled: boolean = false; /** diff --git a/packages/core/src/particle/modules/ParticleGradient.ts b/packages/core/src/particle/modules/ParticleGradient.ts index ff7bb0a5ce..b2eec2507f 100644 --- a/packages/core/src/particle/modules/ParticleGradient.ts +++ b/packages/core/src/particle/modules/ParticleGradient.ts @@ -1,17 +1,15 @@ import { Color } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; /** * Particle gradient. */ export class ParticleGradient { - @deepClone + @property private _colorKeys: GradientColorKey[] = []; - @deepClone + @property private _alphaKeys: GradientAlphaKey[] = []; - @ignoreClone private _colorTypeArray: Float32Array; - @ignoreClone private _alphaTypeArray: Float32Array; private _colorTypeArrayDirty: boolean = false; @@ -208,12 +206,10 @@ export class ParticleGradient { keys.splice(index, 1); } - @ignoreClone private _setColorTypeArrayDirty(): void { this._colorTypeArrayDirty = true; } - @ignoreClone private _setAlphaTypeArrayDirty(): void { this._alphaTypeArrayDirty = true; } @@ -226,7 +222,9 @@ export class GradientColorKey { /** @internal */ _onValueChanged: () => void = null; + @property private _time: number; + @property private _color: Color = new Color(); /** @@ -274,7 +272,9 @@ export class GradientAlphaKey { /** @internal */ _onValueChanged: () => void = null; + @property private _time: number; + @property private _alpha: number; /** diff --git a/packages/core/src/particle/modules/RotationOverLifetimeModule.ts b/packages/core/src/particle/modules/RotationOverLifetimeModule.ts index 557f5b983b..a20cbde5c1 100644 --- a/packages/core/src/particle/modules/RotationOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/RotationOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -27,30 +27,25 @@ export class RotationOverLifetimeModule extends ParticleGeneratorModule { static readonly _maxCurveZProperty = ShaderProperty.getByName("renderer_ROLMaxCurveZ"); /** Specifies whether the rotation is separate on each axis, when disabled, only `rotationZ` is used. */ + @property separateAxes: boolean = false; /** Rotation over lifetime for x axis, in degrees. */ - @deepClone + @property rotationX = new ParticleCompositeCurve(0); /** Rotation over lifetime for y axis, in degrees. */ - @deepClone + @property rotationY = new ParticleCompositeCurve(0); /** Rotation over lifetime for z axis, in degrees. */ - @deepClone + @property rotationZ = new ParticleCompositeCurve(45); /** @internal */ - @ignoreClone _rotationRand = new Rand(0, ParticleRandomSubSeeds.RotationOverLifetime); - @ignoreClone private _rotationMinConstant = new Vector3(); - @ignoreClone private _rotationMaxConstant = new Vector3(); - @ignoreClone private _enableSeparateMacro: ShaderMacro; - @ignoreClone private _modeMacro: ShaderMacro; - @ignoreClone private _isRandomTwoMacro: ShaderMacro; /** diff --git a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts index b8c9c6fb18..3c72630007 100644 --- a/packages/core/src/particle/modules/SizeOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/SizeOverLifetimeModule.ts @@ -1,4 +1,4 @@ -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -23,19 +23,17 @@ export class SizeOverLifetimeModule extends ParticleGeneratorModule { static readonly _maxCurveYProperty = ShaderProperty.getByName("renderer_SOLMaxCurveY"); static readonly _maxCurveZProperty = ShaderProperty.getByName("renderer_SOLMaxCurveZ"); + @property private _separateAxes = false; - @deepClone + @property private _sizeX: ParticleCompositeCurve; - @deepClone + @property private _sizeY: ParticleCompositeCurve; - @deepClone + @property private _sizeZ: ParticleCompositeCurve; - @ignoreClone private _enableSeparateMacro: ShaderMacro; - @ignoreClone private _isCurveMacro: ShaderMacro; - @ignoreClone private _isRandomTwoMacro: ShaderMacro; /** diff --git a/packages/core/src/particle/modules/TextureSheetAnimationModule.ts b/packages/core/src/particle/modules/TextureSheetAnimationModule.ts index c75601c339..807c1758d9 100644 --- a/packages/core/src/particle/modules/TextureSheetAnimationModule.ts +++ b/packages/core/src/particle/modules/TextureSheetAnimationModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector2, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone, shallowClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderMacro } from "../../shader/ShaderMacro"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -24,23 +24,23 @@ export class TextureSheetAnimationModule extends ParticleGeneratorModule { private static readonly _tillingParamsProperty = ShaderProperty.getByName("renderer_TSATillingParams"); /** Frame over time curve of the texture sheet. */ - @deepClone + @property readonly frameOverTime = new ParticleCompositeCurve(new ParticleCurve(new CurveKey(0, 0), new CurveKey(1, 1))); /** Texture sheet animation type. */ + @property type = TextureSheetAnimationType.WholeSheet; /** Cycle count. */ + @property cycleCount = 1; /** @internal */ - @shallowClone + @property _tillingInfo = new Vector3(1, 1, 1); // x:subU, y:subV, z:tileCount /** @internal */ - @ignoreClone _frameOverTimeRand = new Rand(0, ParticleRandomSubSeeds.TextureSheetAnimation); - @deepClone + @property private _tiling = new Vector2(1, 1); - @ignoreClone private _frameCurveMacro: ShaderMacro; /** @@ -92,7 +92,6 @@ export class TextureSheetAnimationModule extends ParticleGeneratorModule { this._frameOverTimeRand.reset(randomSeed, ParticleRandomSubSeeds.TextureSheetAnimation); } - @ignoreClone private _onTilingChanged(): void { const tiling = this.tiling; this._tillingInfo.set(1.0 / tiling.x, 1.0 / tiling.y, tiling.x * tiling.y); diff --git a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts index 4fc1d03196..9c380ebabb 100644 --- a/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts +++ b/packages/core/src/particle/modules/VelocityOverLifetimeModule.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderMacro } from "../../shader"; import { ShaderData } from "../../shader/ShaderData"; import { ShaderProperty } from "../../shader/ShaderProperty"; @@ -29,24 +29,20 @@ export class VelocityOverLifetimeModule extends ParticleGeneratorModule { static readonly _spaceProperty = ShaderProperty.getByName("renderer_VOLSpace"); /** @internal */ - @ignoreClone _velocityRand = new Rand(0, ParticleRandomSubSeeds.VelocityOverLifetime); - @ignoreClone private _velocityMinConstant = new Vector3(); - @ignoreClone private _velocityMaxConstant = new Vector3(); - @ignoreClone private _velocityMacro: ShaderMacro; - @ignoreClone private _randomModeMacro: ShaderMacro; - @deepClone + @property private _velocityX: ParticleCompositeCurve; - @deepClone + @property private _velocityY: ParticleCompositeCurve; - @deepClone + @property private _velocityZ: ParticleCompositeCurve; + @property private _space = ParticleSimulationSpace.Local; /** diff --git a/packages/core/src/particle/modules/shape/BaseShape.ts b/packages/core/src/particle/modules/shape/BaseShape.ts index 9e00891b59..411581ea3e 100644 --- a/packages/core/src/particle/modules/shape/BaseShape.ts +++ b/packages/core/src/particle/modules/shape/BaseShape.ts @@ -1,7 +1,7 @@ import { BoundingBox, MathUtil, Matrix, Quaternion, Rand, Vector2, Vector3 } from "@galacean/engine-math"; import { ParticleShapeType } from "./enums/ParticleShapeType"; import { UpdateFlagManager } from "../../../UpdateFlagManager"; -import { deepClone, ignoreClone } from "../../../clone/CloneManager"; +import { property } from "../../../clone/CloneManager"; /** * Base class for all particle shapes. @@ -19,23 +19,21 @@ export abstract class BaseShape { /** The type of shape to emit particles from. */ abstract readonly shapeType: ParticleShapeType; - @ignoreClone protected _updateManager = new UpdateFlagManager(); + @property private _enabled = true; + @property private _randomDirectionAmount = 0; - @deepClone + @property private _position = new Vector3(0, 0, 0); - @deepClone + @property private _rotation = new Vector3(0, 0, 0); - @deepClone + @property private _scale = new Vector3(1, 1, 1); - @ignoreClone private _matrix = new Matrix(); - @ignoreClone private _transformDirty = false; - @ignoreClone private _hasShapeTransform = false; /** @@ -172,7 +170,6 @@ export abstract class BaseShape { protected abstract _getLocalDirectionRange(outMin: Vector3, outMax: Vector3): void; - @ignoreClone protected _onTransformChanged = (): void => { this._transformDirty = true; const { _position: p, _rotation: r, _scale: s } = this; diff --git a/packages/core/src/particle/modules/shape/BoxShape.ts b/packages/core/src/particle/modules/shape/BoxShape.ts index 7ad762e2e8..3f6eda7f40 100644 --- a/packages/core/src/particle/modules/shape/BoxShape.ts +++ b/packages/core/src/particle/modules/shape/BoxShape.ts @@ -1,5 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; -import { deepClone } from "../../../clone/CloneManager"; +import { property } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeType } from "./enums/ParticleShapeType"; @@ -10,7 +10,7 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class BoxShape extends BaseShape { readonly shapeType = ParticleShapeType.Box; - @deepClone + @property private _size = new Vector3(1, 1, 1); /** diff --git a/packages/core/src/particle/modules/shape/CircleShape.ts b/packages/core/src/particle/modules/shape/CircleShape.ts index cc9156f934..0157ee54dd 100644 --- a/packages/core/src/particle/modules/shape/CircleShape.ts +++ b/packages/core/src/particle/modules/shape/CircleShape.ts @@ -1,4 +1,5 @@ import { MathUtil, Rand, Vector3 } from "@galacean/engine-math"; +import { property } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeArcMode } from "./enums/ParticleShapeArcMode"; @@ -10,9 +11,13 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class CircleShape extends BaseShape { readonly shapeType = ParticleShapeType.Circle; + @property private _radius = 1.0; + @property private _arc = 360.0; + @property private _arcMode = ParticleShapeArcMode.Random; + @property private _arcSpeed = 1.0; /** diff --git a/packages/core/src/particle/modules/shape/ConeShape.ts b/packages/core/src/particle/modules/shape/ConeShape.ts index fec2ee7afa..48c751f9fd 100644 --- a/packages/core/src/particle/modules/shape/ConeShape.ts +++ b/packages/core/src/particle/modules/shape/ConeShape.ts @@ -1,4 +1,5 @@ import { MathUtil, Rand, Vector2, Vector3 } from "@galacean/engine-math"; +import { property } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeType } from "./enums/ParticleShapeType"; @@ -9,9 +10,13 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class ConeShape extends BaseShape { readonly shapeType = ParticleShapeType.Cone; + @property private _angle = 25.0; + @property private _radius = 1.0; + @property private _length = 5.0; + @property private _emitType = ConeEmitType.Base; /** diff --git a/packages/core/src/particle/modules/shape/HemisphereShape.ts b/packages/core/src/particle/modules/shape/HemisphereShape.ts index 62ad271be6..aace6aaa1e 100644 --- a/packages/core/src/particle/modules/shape/HemisphereShape.ts +++ b/packages/core/src/particle/modules/shape/HemisphereShape.ts @@ -1,4 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; +import { property } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeType } from "./enums/ParticleShapeType"; @@ -9,6 +10,7 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class HemisphereShape extends BaseShape { readonly shapeType = ParticleShapeType.Hemisphere; + @property private _radius = 1.0; /** diff --git a/packages/core/src/particle/modules/shape/MeshShape.ts b/packages/core/src/particle/modules/shape/MeshShape.ts index e425003704..2f46239ac6 100644 --- a/packages/core/src/particle/modules/shape/MeshShape.ts +++ b/packages/core/src/particle/modules/shape/MeshShape.ts @@ -1,6 +1,6 @@ import { Rand, Vector3, Vector4 } from "@galacean/engine-math"; import { TypedArray } from "../../../base"; -import { ignoreClone } from "../../../clone/CloneManager"; + import { Entity } from "../../../Entity"; import { VertexElement } from "../../../graphic"; import { MeshModifyFlags } from "../../../graphic/Mesh"; @@ -14,15 +14,10 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class MeshShape extends BaseShape { readonly shapeType = ParticleShapeType.Mesh; - @ignoreClone private _mesh: ModelMesh; - @ignoreClone private _positionBuffer: TypedArray; - @ignoreClone private _normalBuffer: TypedArray; - @ignoreClone private _positionElementInfo = new Vector4(); // x:offset, y:stride, z:isNormalized, w:normalizedScaleFactor - @ignoreClone private _normalElementInfo = new Vector4(); // x:offset, y:stride, z:isNormalized, w:normalizedScaleFactor /** @@ -133,7 +128,6 @@ export class MeshShape extends BaseShape { return typedBuffer; } - @ignoreClone private _onMeshChanged(type: MeshModifyFlags): void { if (type & MeshModifyFlags.VertexElements) { const mesh = this._mesh; diff --git a/packages/core/src/particle/modules/shape/SphereShape.ts b/packages/core/src/particle/modules/shape/SphereShape.ts index 6bfa0caff0..442e90aa14 100644 --- a/packages/core/src/particle/modules/shape/SphereShape.ts +++ b/packages/core/src/particle/modules/shape/SphereShape.ts @@ -1,4 +1,5 @@ import { Rand, Vector3 } from "@galacean/engine-math"; +import { property } from "../../../clone/CloneManager"; import { BaseShape } from "./BaseShape"; import { ShapeUtils } from "./ShapeUtils"; import { ParticleShapeType } from "./enums/ParticleShapeType"; @@ -9,6 +10,7 @@ import { ParticleShapeType } from "./enums/ParticleShapeType"; export class SphereShape extends BaseShape { readonly shapeType = ParticleShapeType.Sphere; + @property private _radius = 1.0; /** diff --git a/packages/core/src/physics/CharacterController.ts b/packages/core/src/physics/CharacterController.ts index f3c9e6f048..f764d44752 100644 --- a/packages/core/src/physics/CharacterController.ts +++ b/packages/core/src/physics/CharacterController.ts @@ -5,16 +5,19 @@ import { Entity } from "../Entity"; import { Collider } from "./Collider"; import { ControllerNonWalkableMode } from "./enums/ControllerNonWalkableMode"; import { ColliderShape } from "./shape"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; /** * The character controllers. */ export class CharacterController extends Collider { + @property private _stepOffset = 0.5; + @property private _nonWalkableMode: ControllerNonWalkableMode = ControllerNonWalkableMode.PreventClimbing; - @deepClone + @property private _upDirection = new Vector3(0, 1, 0); + @property private _slopeLimit = 45; /** @@ -166,7 +169,6 @@ export class CharacterController extends Collider { (this._nativeCollider).getWorldPosition(this.entity.transform.worldPosition); } - @ignoreClone private _setUpDirection(): void { (this._nativeCollider).setUpDirection(this._upDirection); } diff --git a/packages/core/src/physics/Collider.ts b/packages/core/src/physics/Collider.ts index a8a591fe56..0bca7f06a0 100644 --- a/packages/core/src/physics/Collider.ts +++ b/packages/core/src/physics/Collider.ts @@ -1,6 +1,6 @@ import { ICollider, IStaticCollider } from "@galacean/engine-design"; import { BoolUpdateFlag } from "../BoolUpdateFlag"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { ICustomClone } from "../clone/ComponentCloner"; import { Component } from "../Component"; import { DependentMode, dependentComponents } from "../ComponentsDependencies"; @@ -17,15 +17,13 @@ import { ColliderShapeChangeFlag } from "./enums/ColliderShapeChangeFlag"; @dependentComponents(Transform, DependentMode.CheckOnly) export class Collider extends Component implements ICustomClone { /** @internal */ - @ignoreClone _index: number = -1; /** @internal */ - @ignoreClone _nativeCollider: ICollider; - @ignoreClone protected _updateFlag: BoolUpdateFlag; - @deepClone + @property protected _shapes: ColliderShape[] = []; + @property protected _collisionLayerIndex: number = 0; /** diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 380a3b927f..5b5038f96f 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -1,6 +1,6 @@ import { IDynamicCollider } from "@galacean/engine-design"; import { Quaternion, Vector3 } from "@galacean/engine-math"; -import { ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Engine } from "../Engine"; import { Entity } from "../Entity"; import { Collider } from "./Collider"; @@ -15,26 +15,35 @@ export class DynamicCollider extends Collider { private static _tempVector3 = new Vector3(); private static _tempQuat = new Quaternion(); + @property private _linearDamping = 0; + @property private _angularDamping = 0.05; - @ignoreClone private _linearVelocity = new Vector3(); - @ignoreClone private _angularVelocity = new Vector3(); + @property private _mass = 1.0; - @ignoreClone private _centerOfMass = new Vector3(); - @ignoreClone private _inertiaTensor = new Vector3(1, 1, 1); + @property private _maxAngularVelocity = 18000 / Math.PI; + @property private _maxDepenetrationVelocity = 1.0000000331813535e32; + @property private _solverIterations = 4; + @property private _useGravity = true; + @property private _isKinematic = false; + @property private _constraints: DynamicColliderConstraints = 0; + @property private _collisionDetectionMode: CollisionDetectionMode = CollisionDetectionMode.Discrete; + @property private _sleepThreshold = 5e-3; + @property private _automaticCenterOfMass = true; + @property private _automaticInertiaTensor = true; /** @@ -508,17 +517,14 @@ export class DynamicCollider extends Collider { this._automaticInertiaTensor || (this._nativeCollider).setInertiaTensor(this._inertiaTensor); } - @ignoreClone private _setLinearVelocity(): void { (this._nativeCollider).setLinearVelocity(this._linearVelocity); } - @ignoreClone private _setAngularVelocity(): void { (this._nativeCollider).setAngularVelocity(this._angularVelocity); } - @ignoreClone private _handleCenterOfMassChanged(): void { if (this._automaticCenterOfMass) { console.warn( @@ -529,7 +535,6 @@ export class DynamicCollider extends Collider { } } - @ignoreClone private _handleInertiaTensorChanged(): void { if (this._automaticInertiaTensor) { console.warn( diff --git a/packages/core/src/physics/PhysicsMaterial.ts b/packages/core/src/physics/PhysicsMaterial.ts index 3ac16e66d8..fc0ad26555 100644 --- a/packages/core/src/physics/PhysicsMaterial.ts +++ b/packages/core/src/physics/PhysicsMaterial.ts @@ -1,10 +1,13 @@ import { IPhysicsMaterial } from "@galacean/engine-design"; import { Engine } from "../Engine"; +import { defaultCloneMode } from "../clone/CloneManager"; +import { CloneMode } from "../clone/enums/CloneMode"; import { PhysicsMaterialCombineMode } from "./enums/PhysicsMaterialCombineMode"; /** * Material class to represent a set of surface properties. */ +@defaultCloneMode(CloneMode.Assignment) export class PhysicsMaterial { private _bounciness = 0; private _dynamicFriction = 0.6; diff --git a/packages/core/src/physics/joint/HingeJoint.ts b/packages/core/src/physics/joint/HingeJoint.ts index acffbae4b6..883b6cceef 100644 --- a/packages/core/src/physics/joint/HingeJoint.ts +++ b/packages/core/src/physics/joint/HingeJoint.ts @@ -6,20 +6,22 @@ import { HingeJointFlag } from "../enums/HingeJointFlag"; import { Joint } from "./Joint"; import { JointLimits } from "./JointLimits"; import { JointMotor } from "./JointMotor"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { Entity } from "../../Entity"; /** * A joint which behaves in a similar way to a hinge or axle. */ export class HingeJoint extends Joint { - @deepClone + @property private _axis = new Vector3(1, 0, 0); + @property private _hingeFlags = HingeJointFlag.None; + @property private _useSpring = false; - @deepClone + @property private _jointMotor: JointMotor; - @deepClone + @property private _limits: JointLimits; private _angle = 0; private _velocity = 0; @@ -177,7 +179,6 @@ export class HingeJoint extends Joint { } } - @ignoreClone private _onMotorChanged() { const motor = this._jointMotor; if (this._nativeJoint) { @@ -188,7 +189,6 @@ export class HingeJoint extends Joint { } } - @ignoreClone private _onLimitsChanged() { const limits = this._limits; if (limits && this._nativeJoint) { @@ -200,7 +200,6 @@ export class HingeJoint extends Joint { } } - @ignoreClone private _onAxisChanged(): void { //@ts-ignore this._axis._onValueChanged = null; diff --git a/packages/core/src/physics/joint/Joint.ts b/packages/core/src/physics/joint/Joint.ts index 8d9f8af178..8978a9d187 100644 --- a/packages/core/src/physics/joint/Joint.ts +++ b/packages/core/src/physics/joint/Joint.ts @@ -4,7 +4,7 @@ import { Component } from "../../Component"; import { DependentMode, dependentComponents } from "../../ComponentsDependencies"; import { Entity } from "../../Entity"; import { TransformModifyFlags } from "../../Transform"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { Collider } from "../Collider"; import { DynamicCollider } from "../DynamicCollider"; @@ -18,14 +18,16 @@ export abstract class Joint extends Component { private static _tempQuat = new Quaternion(); private static _tempMatrix = new Matrix(); - @deepClone + @property protected _colliderInfo = new JointColliderInfo(); - @deepClone + @property protected _connectedColliderInfo = new JointColliderInfo(); - @ignoreClone protected _nativeJoint: IJoint; + @property private _force = Infinity; + @property private _torque = Infinity; + @property private _automaticConnectedAnchor = true; /** @@ -254,7 +256,6 @@ export abstract class Joint extends Component { this._updateActualAnchor(AnchorOwner.Connected); } - @ignoreClone private _handleConnectedAnchorChanged(): void { if (this._automaticConnectedAnchor) { console.warn("Cannot set connectedAnchor when automaticConnectedAnchor is true."); @@ -263,14 +264,12 @@ export abstract class Joint extends Component { } } - @ignoreClone private _onSelfTransformChanged(type: TransformModifyFlags): void { if (type & TransformModifyFlags.WorldScale) { this._updateActualAnchor(AnchorOwner.Self); } } - @ignoreClone private _onConnectedTransformChanged(type: TransformModifyFlags): void { if (type & TransformModifyFlags.WorldScale) { this._updateActualAnchor(AnchorOwner.Connected); @@ -322,11 +321,14 @@ enum AnchorOwner { * @internal */ class JointColliderInfo { + @property collider: Collider = null; - @deepClone + @property anchor = new Vector3(); - @deepClone + @property actualAnchor = new Vector3(); + @property massScale: number = 1; + @property inertiaScale: number = 1; } diff --git a/packages/core/src/physics/joint/JointLimits.ts b/packages/core/src/physics/joint/JointLimits.ts index 3d7ff811be..5113e9e206 100644 --- a/packages/core/src/physics/joint/JointLimits.ts +++ b/packages/core/src/physics/joint/JointLimits.ts @@ -1,18 +1,23 @@ -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { UpdateFlagManager } from "../../UpdateFlagManager"; /** * JointLimits is used to limit the joints angle. */ export class JointLimits { - @deepClone + @property /** @internal */ _updateFlagManager = new UpdateFlagManager(); + @property private _max = 0; + @property private _min = 0; + @property private _contactDistance = -1; + @property private _stiffness = 0; + @property private _damping = 0; /** diff --git a/packages/core/src/physics/joint/JointMotor.ts b/packages/core/src/physics/joint/JointMotor.ts index 0f381c1a6c..4d885b82bd 100644 --- a/packages/core/src/physics/joint/JointMotor.ts +++ b/packages/core/src/physics/joint/JointMotor.ts @@ -1,17 +1,21 @@ -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { UpdateFlagManager } from "../../UpdateFlagManager"; /** * The JointMotor is used to motorize a joint. */ export class JointMotor { - @deepClone + @property /** @internal */ _updateFlagManager = new UpdateFlagManager(); + @property private _targetVelocity = 0; + @property private _forceLimit = Number.MAX_VALUE; + @property private _gearRatio = 1.0; + @property private _freeSpin = false; /** diff --git a/packages/core/src/physics/joint/SpringJoint.ts b/packages/core/src/physics/joint/SpringJoint.ts index 06708702fc..380c215a7f 100644 --- a/packages/core/src/physics/joint/SpringJoint.ts +++ b/packages/core/src/physics/joint/SpringJoint.ts @@ -1,5 +1,6 @@ import { ISpringJoint } from "@galacean/engine-design"; import { Engine } from "../../Engine"; +import { property } from "../../clone/CloneManager"; import { Collider } from "../Collider"; import { Joint } from "./Joint"; @@ -7,10 +8,15 @@ import { Joint } from "./Joint"; * A joint that maintains an upper or lower bound (or both) on the distance between two points on different objects. */ export class SpringJoint extends Joint { + @property private _minDistance: number = 0; + @property private _maxDistance: number = 0; + @property private _tolerance: number = 0.25; + @property private _stiffness: number = 0; + @property private _damping: number = 0; /** diff --git a/packages/core/src/physics/shape/BoxColliderShape.ts b/packages/core/src/physics/shape/BoxColliderShape.ts index 12c32a0a4b..b8b7c58b4a 100644 --- a/packages/core/src/physics/shape/BoxColliderShape.ts +++ b/packages/core/src/physics/shape/BoxColliderShape.ts @@ -2,13 +2,13 @@ import { ColliderShape } from "./ColliderShape"; import { IBoxColliderShape } from "@galacean/engine-design"; import { Vector3 } from "@galacean/engine-math"; import { Engine } from "../../Engine"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; /** * Physical collider shape for box. */ export class BoxColliderShape extends ColliderShape { - @deepClone + @property private _size: Vector3 = new Vector3(1, 1, 1); /** @@ -41,7 +41,6 @@ export class BoxColliderShape extends ColliderShape { this._setSize(); } - @ignoreClone private _setSize(): void { (this._nativeShape).setSize(this._size); } diff --git a/packages/core/src/physics/shape/CapsuleColliderShape.ts b/packages/core/src/physics/shape/CapsuleColliderShape.ts index 9df22d6c8c..74070b824c 100644 --- a/packages/core/src/physics/shape/CapsuleColliderShape.ts +++ b/packages/core/src/physics/shape/CapsuleColliderShape.ts @@ -2,13 +2,17 @@ import { ColliderShape } from "./ColliderShape"; import { ICapsuleColliderShape } from "@galacean/engine-design"; import { Engine } from "../../Engine"; import { ColliderShapeUpAxis } from "../enums/ColliderShapeUpAxis"; +import { property } from "../../clone/CloneManager"; /** * Physical collider shape for capsule. */ export class CapsuleColliderShape extends ColliderShape { + @property private _radius: number = 1; + @property private _height: number = 2; + @property private _upAxis: ColliderShapeUpAxis = ColliderShapeUpAxis.Y; /** diff --git a/packages/core/src/physics/shape/ColliderShape.ts b/packages/core/src/physics/shape/ColliderShape.ts index 2ac82319fd..fd1679af43 100644 --- a/packages/core/src/physics/shape/ColliderShape.ts +++ b/packages/core/src/physics/shape/ColliderShape.ts @@ -2,7 +2,7 @@ import { IColliderShape } from "@galacean/engine-design"; import { PhysicsMaterial } from "../PhysicsMaterial"; import { Vector3 } from "@galacean/engine-math"; import { Collider } from "../Collider"; -import { deepClone, ignoreClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ICustomClone } from "../../clone/ComponentCloner"; import { Engine } from "../../Engine"; import { ColliderShapeChangeFlag } from "../enums/ColliderShapeChangeFlag"; @@ -16,17 +16,18 @@ export abstract class ColliderShape implements ICustomClone { /** @internal */ _collider: Collider; /** @internal */ - @ignoreClone _nativeShape: IColliderShape; - @ignoreClone protected _id: number; + @property protected _material: PhysicsMaterial; + @property private _isTrigger: boolean = false; - @deepClone + @property private _rotation: Vector3 = new Vector3(); - @deepClone + @property private _position: Vector3 = new Vector3(); + @property private _contactOffset: number = 0.02; /** @@ -34,6 +35,7 @@ export abstract class ColliderShape implements ICustomClone { * @beta * Whether raycast can select it. */ + @property isSceneQuery: boolean = true; /** @@ -190,13 +192,11 @@ export abstract class ColliderShape implements ICustomClone { this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); } - @ignoreClone private _setPosition(): void { this._nativeShape?.setPosition(this._position); this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); } - @ignoreClone private _setRotation(): void { this._nativeShape?.setRotation(this._rotation); this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property); diff --git a/packages/core/src/physics/shape/MeshColliderShape.ts b/packages/core/src/physics/shape/MeshColliderShape.ts index 51a46ab0e0..ece66ee782 100644 --- a/packages/core/src/physics/shape/MeshColliderShape.ts +++ b/packages/core/src/physics/shape/MeshColliderShape.ts @@ -5,15 +5,19 @@ import { Vector3 } from "@galacean/engine-math"; import { DynamicCollider } from "../DynamicCollider"; import { MeshColliderShapeCookingFlag } from "../enums/MeshColliderShapeCookingFlag"; import { ColliderShape } from "./ColliderShape"; +import { property } from "../../clone/CloneManager"; /** * Collider shape based on mesh geometry, supporting both convex hull and triangle mesh modes. */ export class MeshColliderShape extends ColliderShape { + @property private _mesh: ModelMesh = null; + @property private _isConvex = false; private _positions: Vector3[] = null; private _indices: Uint8Array | Uint16Array | Uint32Array | null = null; + @property private _cookingFlags = MeshColliderShapeCookingFlag.Cleaning | MeshColliderShapeCookingFlag.VertexWelding; private _isShapeAttached = false; @@ -98,6 +102,21 @@ export class MeshColliderShape extends ColliderShape { return super.getClosestPoint(point, outClosestPoint); } + /** + * @internal + */ + override _cloneTo(target: MeshColliderShape): void { + // The mesh is shared by the clone, but the extracted buffers and the native PhysX shape are not + // cloned — rebuild the native shape from the cloned mesh so the clone has real collision geometry + // The Collider re-attaches it afterwards via `_addNativeShape` + const mesh = target._mesh; + if (mesh && target._extractMeshData(mesh)) { + target._createNativeShape(); + } else { + super._cloneTo(target); + } + } + /** * @internal */ diff --git a/packages/core/src/physics/shape/SphereColliderShape.ts b/packages/core/src/physics/shape/SphereColliderShape.ts index 13cddc9b44..5beef27a91 100644 --- a/packages/core/src/physics/shape/SphereColliderShape.ts +++ b/packages/core/src/physics/shape/SphereColliderShape.ts @@ -1,11 +1,13 @@ import { ColliderShape } from "./ColliderShape"; import { ISphereColliderShape } from "@galacean/engine-design"; import { Engine } from "../../Engine"; +import { property } from "../../clone/CloneManager"; /** * Physical collider shape for sphere. */ export class SphereColliderShape extends ColliderShape { + @property private _radius: number = 1; /** diff --git a/packages/core/src/postProcess/PostProcess.ts b/packages/core/src/postProcess/PostProcess.ts index 66521a02f1..4b1913c54a 100644 --- a/packages/core/src/postProcess/PostProcess.ts +++ b/packages/core/src/postProcess/PostProcess.ts @@ -1,5 +1,5 @@ import { Logger } from "../base"; -import { deepClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Component } from "../Component"; import { Layer } from "../Layer"; import { PostProcessEffect } from "./PostProcessEffect"; @@ -11,18 +11,22 @@ export class PostProcess extends Component { /** * The layer to which the PostProcess belongs. */ + @property layer = Layer.Layer0; /** * The outer distance to start blending from, only takes effect when the `isGlobal` is false. */ + @property blendDistance = 0; /** @internal */ - @deepClone + @property _effects: PostProcessEffect[] = []; + @property private _priority = 0; + @property private _isGlobal = true; /** diff --git a/packages/core/src/postProcess/PostProcessEffect.ts b/packages/core/src/postProcess/PostProcessEffect.ts index a2a29d382a..43894e7704 100644 --- a/packages/core/src/postProcess/PostProcessEffect.ts +++ b/packages/core/src/postProcess/PostProcessEffect.ts @@ -1,9 +1,11 @@ +import { property } from "../clone/CloneManager"; import { PostProcessEffectParameter } from "./PostProcessEffectParameter"; /** * The base class for post process effect. */ export class PostProcessEffect { + @property private _enabled = true; private _parameters: PostProcessEffectParameter[] = []; private _parameterInitialized = false; diff --git a/packages/core/src/postProcess/PostProcessEffectParameter.ts b/packages/core/src/postProcess/PostProcessEffectParameter.ts index 899860ba76..cdb882fa74 100644 --- a/packages/core/src/postProcess/PostProcessEffectParameter.ts +++ b/packages/core/src/postProcess/PostProcessEffectParameter.ts @@ -1,4 +1,5 @@ import { Color, MathUtil, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; +import { property } from "../clone/CloneManager"; import { Texture } from "../texture"; /** @@ -10,9 +11,12 @@ export abstract class PostProcessEffectParameter { /** * Whether the parameter is enabled. */ + @property enabled = true; + @property protected _needLerp = false; + @property protected _value: T; /** diff --git a/packages/core/src/postProcess/effects/BloomEffect.ts b/packages/core/src/postProcess/effects/BloomEffect.ts index 8e35f0e7e4..060c6a7653 100644 --- a/packages/core/src/postProcess/effects/BloomEffect.ts +++ b/packages/core/src/postProcess/effects/BloomEffect.ts @@ -1,4 +1,5 @@ import { Color } from "@galacean/engine-math"; +import { property } from "../../clone/CloneManager"; import { ShaderMacro, ShaderProperty } from "../../shader"; import { PostProcessEffect } from "../PostProcessEffect"; @@ -55,42 +56,50 @@ export class BloomEffect extends PostProcessEffect { * Controls whether to use bicubic sampling instead of bilinear sampling for the upSampling passes. * @remarks This is slightly more expensive but helps getting smoother visuals. */ + @property highQualityFiltering = new PostProcessEffectBoolParameter(false); /** * Controls the starting resolution that this effect begins processing. */ + @property downScale = new PostProcessEffectEnumParameter(BloomDownScaleMode, BloomDownScaleMode.Half); /** * Specifies a Texture to add smudges or dust to the bloom effect. */ + @property dirtTexture = new PostProcessEffectTextureParameter(null); /** * Set the level of brightness to filter out pixels under this level. * @remarks This value is expressed in linear. */ + @property threshold = new PostProcessEffectFloatParameter(0.8, 0); /** * Controls the radius of the bloom effect. */ + @property scatter = new PostProcessEffectFloatParameter(0.7, 0, 1); /** * Controls the strength of the bloom effect. */ + @property intensity = new PostProcessEffectFloatParameter(0, 0); /** * Controls the strength of the lens dirt. */ + @property dirtIntensity = new PostProcessEffectFloatParameter(0, 0); /** * Specifies the tint of the bloom effect. */ + @property tint = new PostProcessEffectColorParameter(new Color(1, 1, 1, 1)); /** @inheritdoc */ diff --git a/packages/core/src/postProcess/effects/TonemappingEffect.ts b/packages/core/src/postProcess/effects/TonemappingEffect.ts index c6e3e0f9a2..a38543dcb0 100644 --- a/packages/core/src/postProcess/effects/TonemappingEffect.ts +++ b/packages/core/src/postProcess/effects/TonemappingEffect.ts @@ -1,3 +1,4 @@ +import { property } from "../../clone/CloneManager"; import { ShaderMacro } from "../../shader"; import { PostProcessEffect } from "../PostProcessEffect"; import { PostProcessEffectEnumParameter } from "../PostProcessEffectParameter"; @@ -28,5 +29,6 @@ export class TonemappingEffect extends PostProcessEffect { /** * Use this to select a tonemapping algorithm to use. */ + @property mode = new PostProcessEffectEnumParameter(TonemappingMode, TonemappingMode.Neutral); } diff --git a/packages/core/src/shader/Shader.ts b/packages/core/src/shader/Shader.ts index d17d8630a8..4530b5a6ba 100644 --- a/packages/core/src/shader/Shader.ts +++ b/packages/core/src/shader/Shader.ts @@ -2,6 +2,8 @@ import { IShaderCompiler, IPrecompiledShader } from "@galacean/engine-design"; import { Color } from "@galacean/engine-math"; import { Engine } from "../Engine"; import { IReferable } from "../asset/IReferable"; +import { CloneMode } from "../clone/enums/CloneMode"; +import { defaultCloneMode } from "../clone/CloneManager"; import { ShaderMacro } from "./ShaderMacro"; import { ShaderMacroCollection } from "./ShaderMacroCollection"; import { ShaderPass } from "./ShaderPass"; @@ -21,6 +23,7 @@ import { RenderState } from "./state/RenderState"; /** * Shader for rendering. */ +@defaultCloneMode(CloneMode.Assignment) export class Shader implements IReferable { /** @internal */ static readonly _compileMacros: ShaderMacroCollection = new ShaderMacroCollection(); diff --git a/packages/core/src/shader/ShaderData.ts b/packages/core/src/shader/ShaderData.ts index a3bb7992b8..458de47b6c 100644 --- a/packages/core/src/shader/ShaderData.ts +++ b/packages/core/src/shader/ShaderData.ts @@ -1,7 +1,7 @@ import { IClone } from "@galacean/engine-design"; import { Color, Matrix, Vector2, Vector3, Vector4 } from "@galacean/engine-math"; import { IReferable } from "../asset/IReferable"; -import { CloneManager, ignoreClone } from "../clone/CloneManager"; +import { CloneManager } from "../clone/CloneManager"; import { Texture } from "../texture/Texture"; import { ShaderMacro } from "./ShaderMacro"; import { ShaderMacroCollection } from "./ShaderMacroCollection"; @@ -14,20 +14,14 @@ import { ShaderPropertyType } from "./enums/ShaderPropertyType"; */ export class ShaderData implements IReferable, IClone { /** @internal */ - @ignoreClone _group: ShaderDataGroup; /** @internal */ - @ignoreClone _propertyValueMap: Record = Object.create(null); /** @internal */ - @ignoreClone _macroCollection: ShaderMacroCollection = new ShaderMacroCollection(); - @ignoreClone private _batchSharedFields: number[] = []; - @ignoreClone private _macroMap: Record = Object.create(null); - @ignoreClone private _refCount: number = 0; /** @@ -608,7 +602,7 @@ export class ShaderData implements IReferable, IClone { } cloneTo(target: ShaderData): void { - CloneManager.deepCloneObject(this._macroCollection, target._macroCollection, new Map()); + CloneManager.copyProperties(this._macroCollection, target._macroCollection, new Map()); Object.assign(target._macroMap, this._macroMap); const referCount = target._getReferCount(); const propertyValueMap = this._propertyValueMap; @@ -639,6 +633,17 @@ export class ShaderData implements IReferable, IClone { } } + /** + * @internal + * Entity-clone hook (invoked by CloneManager for a `@property` ShaderData field). Delegates to + * `cloneTo` so a cloned component's ShaderData carries its macros / property values instead of being + * left at constructor defaults. `cloneTo` balances texture ref counts via the target's own ref + * count, so this composes with the central clone gate without double-counting. + */ + _cloneTo(target: ShaderData): void { + this.cloneTo(target); + } + /** * @internal */ diff --git a/packages/core/src/shader/ShaderMacro.ts b/packages/core/src/shader/ShaderMacro.ts index 59507aaced..aab12643c9 100644 --- a/packages/core/src/shader/ShaderMacro.ts +++ b/packages/core/src/shader/ShaderMacro.ts @@ -1,8 +1,11 @@ import { ShaderMacroCollection } from "./ShaderMacroCollection"; +import { CloneMode } from "../clone/enums/CloneMode"; +import { defaultCloneMode } from "../clone/CloneManager"; /** * Shader macro。 */ +@defaultCloneMode(CloneMode.Assignment) export class ShaderMacro { /** @internal */ static _macroMaskMap: ShaderMacro[][] = []; diff --git a/packages/core/src/shader/ShaderTagKey.ts b/packages/core/src/shader/ShaderTagKey.ts index 938a87a50a..f0632a5c16 100644 --- a/packages/core/src/shader/ShaderTagKey.ts +++ b/packages/core/src/shader/ShaderTagKey.ts @@ -1,6 +1,10 @@ +import { CloneMode } from "../clone/enums/CloneMode"; +import { defaultCloneMode } from "../clone/CloneManager"; + /** * Shader tag key. */ +@defaultCloneMode(CloneMode.Assignment) export class ShaderTagKey { private static _nameCounter: number = 0; private static _nameMap: Record = Object.create(null); diff --git a/packages/core/src/shader/state/BlendState.ts b/packages/core/src/shader/state/BlendState.ts index 870526d0ab..d1dac72bdb 100644 --- a/packages/core/src/shader/state/BlendState.ts +++ b/packages/core/src/shader/state/BlendState.ts @@ -2,7 +2,7 @@ import { IHardwareRenderer } from "@galacean/engine-design"; import { Color } from "@galacean/engine-math"; import { RenderStateElementMap } from "../../BasicResources"; import { GLCapabilityType } from "../../base/Constant"; -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../ShaderData"; import { ShaderProperty } from "../ShaderProperty"; import { BlendFactor } from "../enums/BlendFactor"; @@ -73,12 +73,13 @@ export class BlendState { } /** The blend state of the render target. */ - @deepClone + @property readonly targetBlendState: RenderTargetBlendState = new RenderTargetBlendState(); /** Constant blend color. */ - @deepClone + @property readonly blendColor: Color = new Color(0, 0, 0, 0); /** Whether to use (Alpha-to-Coverage) technology. */ + @property alphaToCoverage: boolean = false; /** diff --git a/packages/core/src/shader/state/DepthState.ts b/packages/core/src/shader/state/DepthState.ts index 9757e22fda..e236ae2dd7 100644 --- a/packages/core/src/shader/state/DepthState.ts +++ b/packages/core/src/shader/state/DepthState.ts @@ -1,5 +1,6 @@ import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../ShaderData"; import { ShaderProperty } from "../ShaderProperty"; import { CompareFunction } from "../enums/CompareFunction"; @@ -34,10 +35,13 @@ export class DepthState { } /** Whether to enable the depth test. */ + @property enabled: boolean = true; /** Depth comparison function. */ + @property compareFunction: CompareFunction = CompareFunction.Less; /** Whether the depth value can be written.*/ + @property writeEnabled: boolean = true; /** diff --git a/packages/core/src/shader/state/RasterState.ts b/packages/core/src/shader/state/RasterState.ts index 660abff343..dc465f7833 100644 --- a/packages/core/src/shader/state/RasterState.ts +++ b/packages/core/src/shader/state/RasterState.ts @@ -1,5 +1,6 @@ import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../ShaderData"; import { ShaderProperty } from "../ShaderProperty"; import { CullMode } from "../enums/CullMode"; @@ -11,10 +12,13 @@ import { RenderState } from "./RenderState"; */ export class RasterState { /** Specifies whether or not front- and/or back-facing polygons can be culled. */ + @property cullMode: CullMode = CullMode.Back; /** The multiplier by which an implementation-specific value is multiplied with to create a constant depth offset. */ + @property depthBias: number = 0; /** The scale factor for the variable depth offset for each polygon. */ + @property slopeScaledDepthBias: number = 0; /** @internal */ diff --git a/packages/core/src/shader/state/RenderState.ts b/packages/core/src/shader/state/RenderState.ts index 502bfd7d83..3938376bc6 100644 --- a/packages/core/src/shader/state/RenderState.ts +++ b/packages/core/src/shader/state/RenderState.ts @@ -1,7 +1,7 @@ import { ShaderData, ShaderProperty } from ".."; import { RenderStateElementMap } from "../../BasicResources"; import { Engine } from "../../Engine"; -import { deepClone } from "../../clone/CloneManager"; +import { property } from "../../clone/CloneManager"; import { RenderQueueType } from "../enums/RenderQueueType"; import { RenderStateElementKey } from "../enums/RenderStateElementKey"; import { BlendState } from "./BlendState"; @@ -14,19 +14,20 @@ import { StencilState } from "./StencilState"; */ export class RenderState { /** Blend state. */ - @deepClone + @property readonly blendState: BlendState = new BlendState(); /** Depth state. */ - @deepClone + @property readonly depthState: DepthState = new DepthState(); /** Stencil state. */ - @deepClone + @property readonly stencilState: StencilState = new StencilState(); /** Raster state. */ - @deepClone + @property readonly rasterState: RasterState = new RasterState(); /** Render queue type. */ + @property renderQueueType: RenderQueueType = RenderQueueType.Opaque; /** diff --git a/packages/core/src/shader/state/RenderTargetBlendState.ts b/packages/core/src/shader/state/RenderTargetBlendState.ts index 88e3d3f62e..771b8f69cb 100644 --- a/packages/core/src/shader/state/RenderTargetBlendState.ts +++ b/packages/core/src/shader/state/RenderTargetBlendState.ts @@ -1,3 +1,4 @@ +import { property } from "../../clone/CloneManager"; import { BlendOperation } from "../enums/BlendOperation"; import { BlendFactor } from "../enums/BlendFactor"; import { ColorWriteMask } from "../enums/ColorWriteMask"; @@ -7,19 +8,27 @@ import { ColorWriteMask } from "../enums/ColorWriteMask"; */ export class RenderTargetBlendState { /** Whether to enable blend. */ + @property enabled: boolean = false; /** color (RGB) blend operation. */ + @property colorBlendOperation: BlendOperation = BlendOperation.Add; /** alpha (A) blend operation. */ + @property alphaBlendOperation: BlendOperation = BlendOperation.Add; /** color blend factor (RGB) for source. */ + @property sourceColorBlendFactor: BlendFactor = BlendFactor.One; /** alpha blend factor (A) for source. */ + @property sourceAlphaBlendFactor: BlendFactor = BlendFactor.One; /** color blend factor (RGB) for destination. */ + @property destinationColorBlendFactor: BlendFactor = BlendFactor.Zero; /** alpha blend factor (A) for destination. */ + @property destinationAlphaBlendFactor: BlendFactor = BlendFactor.Zero; /** color mask. */ + @property colorWriteMask: ColorWriteMask = ColorWriteMask.All; } diff --git a/packages/core/src/shader/state/StencilState.ts b/packages/core/src/shader/state/StencilState.ts index a479c2fcde..346b76a3c2 100644 --- a/packages/core/src/shader/state/StencilState.ts +++ b/packages/core/src/shader/state/StencilState.ts @@ -1,5 +1,6 @@ import { IHardwareRenderer } from "@galacean/engine-design"; import { RenderStateElementMap } from "../../BasicResources"; +import { property } from "../../clone/CloneManager"; import { ShaderData } from "../ShaderData"; import { ShaderProperty } from "../ShaderProperty"; import { CompareFunction } from "../enums/CompareFunction"; @@ -58,28 +59,40 @@ export class StencilState { } /** Whether to enable stencil test. */ + @property enabled: boolean = false; /** Write the reference value of the stencil buffer. */ + @property referenceValue: number = 0; /** Specifying a bit-wise mask that is used to AND the reference value and the stored stencil value when the test is done. */ + @property mask: number = 0xff; /** Specifying a bit mask to enable or disable writing of individual bits in the stencil planes. */ + @property writeMask: number = 0xff; /** The comparison function of the reference value of the front face of the geometry and the current buffer storage value. */ + @property compareFunctionFront: CompareFunction = CompareFunction.Always; /** The comparison function of the reference value of the back of the geometry and the current buffer storage value. */ + @property compareFunctionBack: CompareFunction = CompareFunction.Always; /** specifying the function to use for front face when both the stencil test and the depth test pass. */ + @property passOperationFront: StencilOperation = StencilOperation.Keep; /** specifying the function to use for back face when both the stencil test and the depth test pass. */ + @property passOperationBack: StencilOperation = StencilOperation.Keep; /** specifying the function to use for front face when the stencil test fails. */ + @property failOperationFront: StencilOperation = StencilOperation.Keep; /** specifying the function to use for back face when the stencil test fails. */ + @property failOperationBack: StencilOperation = StencilOperation.Keep; /** specifying the function to use for front face when the stencil test passes, but the depth test fails. */ + @property zFailOperationFront: StencilOperation = StencilOperation.Keep; /** specifying the function to use for back face when the stencil test passes, but the depth test fails. */ + @property zFailOperationBack: StencilOperation = StencilOperation.Keep; /** diff --git a/packages/core/src/trail/TrailRenderer.ts b/packages/core/src/trail/TrailRenderer.ts index 6485d658ca..bf359e7518 100644 --- a/packages/core/src/trail/TrailRenderer.ts +++ b/packages/core/src/trail/TrailRenderer.ts @@ -2,7 +2,7 @@ import { BoundingBox, Color, Vector2, Vector3, Vector4 } from "@galacean/engine- import { Entity } from "../Entity"; import { RenderContext } from "../RenderPipeline/RenderContext"; import { Renderer, RendererUpdateFlags } from "../Renderer"; -import { deepClone, ignoreClone } from "../clone/CloneManager"; +import { property } from "../clone/CloneManager"; import { Buffer } from "../graphic/Buffer"; import { Primitive } from "../graphic/Primitive"; import { SubPrimitive } from "../graphic/SubPrimitive"; @@ -41,72 +41,57 @@ export class TrailRenderer extends Renderer { private static _tempVector3 = new Vector3(); /** Whether the trail is being created as the object moves. */ + @property emitting = true; /** The minimum distance the object must move before a new trail segment is added, in world units. */ + @property minVertexDistance = 0.1; /** The curve describing the trail width from start to end. */ - @deepClone + @property widthCurve = new ParticleCurve(new CurveKey(0, 1), new CurveKey(1, 1)); /** The gradient describing the trail color from start to end. */ - @deepClone + @property colorGradient = new ParticleGradient( [new GradientColorKey(0, new Color(1, 1, 1, 1)), new GradientColorKey(1, new Color(1, 1, 1, 1))], [new GradientAlphaKey(0, 1), new GradientAlphaKey(1, 1)] ); // Shader parameters - @deepClone + @property private _trailParams = new Vector4(TrailTextureMode.Stretch, 1.0, 1.0, 0); // x: textureMode, y: textureScaleX, z: textureScaleY - @deepClone + @property private _textureScale = new Vector2(1.0, 1.0); - @ignoreClone private _distanceParams = new Vector2(); // x: headDistance, y: tailDistance - @ignoreClone private _curveMaxTime = new Vector4(); // x: colorMaxTime, y: alphaMaxTime, z: widthMaxTime + @property private _time = 5.0; // Geometry and rendering - @ignoreClone private _primitive: Primitive; - @ignoreClone private _mainSubPrimitive: SubPrimitive; - @ignoreClone private _wrapSubPrimitive: SubPrimitive; - @ignoreClone private _vertexBuffer: Buffer; - @ignoreClone private _vertices: Float32Array; // Point management (circular buffer state) - @ignoreClone private _firstActiveElement = 0; - @ignoreClone private _firstNewElement = 0; - @ignoreClone private _firstFreeElement = 0; - @ignoreClone private _firstRetiredElement = 0; - @ignoreClone private _currentPointCapacity = 0; - @ignoreClone private _bufferResized = false; // Position tracking - @ignoreClone private _lastPosition = new Vector3(); - @ignoreClone private _hasLastPosition = false; - @ignoreClone private _cumulativeDistance = 0; // Total distance traveled since trail start // Time tracking - @ignoreClone private _playTime = 0; - @ignoreClone private _lastPlayTimeUpdateFrameCount = -1; /** @@ -612,7 +597,6 @@ export class TrailRenderer extends Renderer { context.camera._renderPipeline.pushRenderElement(context, renderElement); } - @ignoreClone private _onTextureScaleChanged(): void { this._trailParams.y = this._textureScale.x; this._trailParams.z = this._textureScale.y; diff --git a/packages/ui/src/component/UICanvas.ts b/packages/ui/src/component/UICanvas.ts index a69cd846bf..1c032c9eff 100644 --- a/packages/ui/src/component/UICanvas.ts +++ b/packages/ui/src/component/UICanvas.ts @@ -14,10 +14,8 @@ import { RenderElement, Vector2, Vector3, - assignmentClone, - deepClone, dependentComponents, - ignoreClone + property } from "@galacean/engine"; import { Utils } from "../Utils"; import { UIBatchSorter } from "./UIBatchSorter"; @@ -44,61 +42,47 @@ export class UICanvas extends Component implements IElement { private static _tempMat: Matrix = new Matrix(); /** @internal */ - @ignoreClone _canvasIndex: number = -1; /** @internal */ _rootCanvas: UICanvas; /** @internal */ - @ignoreClone _indexInRootCanvas: number = -1; /** @internal */ - @ignoreClone _isRootCanvasDirty: boolean = false; /** @internal */ - @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _isRootCanvas: boolean = false; /** @internal */ - @ignoreClone _renderElements: RenderElement[] = []; /** @internal */ - @ignoreClone _batchedRenderElements: RenderElement[] = []; /** @internal */ - @ignoreClone _sortDistance: number = 0; /** @internal */ - @ignoreClone _orderedRenderers: UIRenderer[] = []; /** @internal */ - @ignoreClone _realRenderMode: number = CanvasRealRenderMode.None; /** @internal */ - @ignoreClone _disorderedElements: DisorderedArray = new DisorderedArray(); - @ignoreClone private _renderMode = CanvasRenderMode.WorldSpace; + @property private _camera: Camera; private _cameraObserver: Camera; - @assignmentClone + @property private _resolutionAdaptationMode = ResolutionAdaptationMode.HeightAdaptation; - @assignmentClone + @property private _sortOrder: number = 0; - @assignmentClone + @property private _distance: number = 10; - @deepClone + @property private _referenceResolution: Vector2 = new Vector2(800, 600); - @assignmentClone + @property private _referenceResolutionPerUnit: number = 100; - @ignoreClone private _hierarchyVersion: number = -1; - @ignoreClone private _center: Vector3 = new Vector3(); - @ignoreClone private _centerDirtyFlag: BoolUpdateFlag; /** @@ -404,7 +388,6 @@ export class UICanvas extends Component implements IElement { /** * @internal */ - @ignoreClone _rootCanvasListener(flag: number, param: any): void { if (this._isRootCanvas) { if (flag === EntityModifyFlags.Parent) { @@ -574,7 +557,6 @@ export class UICanvas extends Component implements IElement { } } - @ignoreClone private _onCameraModifyListener(flag: CameraModifyFlags): void { if (this._realRenderMode === CanvasRenderMode.ScreenSpaceCamera) { switch (flag) { @@ -599,7 +581,6 @@ export class UICanvas extends Component implements IElement { } } - @ignoreClone private _onCameraTransformListener(): void { this._realRenderMode === CanvasRenderMode.ScreenSpaceCamera && this._adapterPoseInScreenSpace(); } @@ -614,14 +595,12 @@ export class UICanvas extends Component implements IElement { this.engine.canvas._sizeUpdateFlagManager.removeListener(this._onCanvasSizeListener); } - @ignoreClone private _onCanvasSizeListener(): void { const { canvas } = this.engine; this.entity.transform.setWorldPosition(canvas.width * 0.5, canvas.height * 0.5, 0); this._adapterSizeInScreenSpace(); } - @ignoreClone private _onReferenceResolutionChanged(): void { const realRenderMode = this._realRenderMode; if ( diff --git a/packages/ui/src/component/UIGroup.ts b/packages/ui/src/component/UIGroup.ts index b876e05ec4..03b8721833 100644 --- a/packages/ui/src/component/UIGroup.ts +++ b/packages/ui/src/component/UIGroup.ts @@ -1,51 +1,41 @@ -import { Component, DisorderedArray, Entity, EntityModifyFlags, assignmentClone, ignoreClone } from "@galacean/engine"; +import { Component, DisorderedArray, Entity, EntityModifyFlags, property } from "@galacean/engine"; import { Utils } from "../Utils"; import { IGroupAble } from "../interface/IGroupAble"; import { EntityUIModifyFlags, UICanvas } from "./UICanvas"; export class UIGroup extends Component implements IGroupAble { /** @internal */ - @ignoreClone _indexInGroup: number = -1; /** @internal */ - @ignoreClone _indexInRootCanvas: number = -1; /** @internal */ _group: UIGroup; /** @internal */ _rootCanvas: UICanvas; /** @internal */ - @ignoreClone _disorderedElements: DisorderedArray = new DisorderedArray(); /** @internal */ - @ignoreClone _globalAlpha = 1; /** @internal */ - @ignoreClone _globalInteractive = true; - @assignmentClone + @property private _alpha = 1; - @assignmentClone + @property private _interactive = true; - @assignmentClone + @property private _ignoreParentGroup = false; /** @internal */ - @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _groupListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _isRootCanvasDirty: boolean = false; /** @internal */ - @ignoreClone _isGroupDirty: boolean = false; /** @internal */ - @ignoreClone _groupDirtyFlags: number = GroupModifyFlags.None; /** @@ -171,7 +161,6 @@ export class UIGroup extends Component implements IGroupAble { /** * @internal */ - @ignoreClone _groupListener(flag: number): void { if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) { Utils.setGroupDirty(this); @@ -181,7 +170,6 @@ export class UIGroup extends Component implements IGroupAble { /** * @internal */ - @ignoreClone _rootCanvasListener(flag: number): void { if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.CanvasEnableInScene) { Utils.setRootCanvasDirty(this); diff --git a/packages/ui/src/component/UIRenderer.ts b/packages/ui/src/component/UIRenderer.ts index 82af6480bd..f330f81032 100644 --- a/packages/ui/src/component/UIRenderer.ts +++ b/packages/ui/src/component/UIRenderer.ts @@ -13,10 +13,8 @@ import { ShaderProperty, Vector3, Vector4, - assignmentClone, - deepClone, dependentComponents, - ignoreClone + property } from "@galacean/engine"; import { Utils } from "../Utils"; import { UIHitResult } from "../input/UIHitResult"; @@ -42,37 +40,30 @@ export class UIRenderer extends Renderer implements IGraphics { * Custom boundary for raycast detection. * @remarks this is based on `this.entity.transform`. */ - @deepClone + @property raycastPadding: Vector4 = new Vector4(0, 0, 0, 0); /** @internal */ _rootCanvas: UICanvas; /** @internal */ - @ignoreClone _indexInRootCanvas: number = -1; /** @internal */ - @ignoreClone _isRootCanvasDirty: boolean = false; /** @internal */ - @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ _group: UIGroup; /** @internal */ - @ignoreClone _indexInGroup: number = -1; /** @internal */ - @ignoreClone _isGroupDirty: boolean = false; /** @internal */ - @ignoreClone _groupListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _subChunk; - @assignmentClone + @property private _raycastEnabled: boolean = true; - @deepClone + @property protected _color: Color = new Color(1, 1, 1, 1); /** @@ -191,7 +182,6 @@ export class UIRenderer extends Renderer implements IGraphics { /** * @internal */ - @ignoreClone _groupListener(flag: number): void { if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) { Utils.setGroupDirty(this); @@ -201,7 +191,6 @@ export class UIRenderer extends Renderer implements IGraphics { /** * @internal */ - @ignoreClone _rootCanvasListener(flag: number, entity: Entity): void { switch (flag) { case EntityModifyFlags.Parent: @@ -224,7 +213,6 @@ export class UIRenderer extends Renderer implements IGraphics { } } - @ignoreClone private _onColorChanged(): void { this._dirtyUpdateFlag |= UIRendererUpdateFlags.Color; } diff --git a/packages/ui/src/component/UITransform.ts b/packages/ui/src/component/UITransform.ts index 9144373472..1c072d18c4 100644 --- a/packages/ui/src/component/UITransform.ts +++ b/packages/ui/src/component/UITransform.ts @@ -1,13 +1,4 @@ -import { - Entity, - MathUtil, - Rect, - Transform, - TransformModifyFlags, - Vector2, - deepClone, - ignoreClone -} from "@galacean/engine"; +import { Entity, MathUtil, Rect, Transform, TransformModifyFlags, Vector2, property } from "@galacean/engine"; import { HorizontalAlignmentMode } from "../enums/HorizontalAlignmentMode"; import { VerticalAlignmentMode } from "../enums/VerticalAlignmentMode"; @@ -15,20 +6,26 @@ import { VerticalAlignmentMode } from "../enums/VerticalAlignmentMode"; * The Transform component exclusive to the UI element. */ export class UITransform extends Transform { - @ignoreClone private _size = new Vector2(100, 100); - @ignoreClone private _pivot = new Vector2(0.5, 0.5); - @deepClone + @property private _rect = new Rect(-50, -50, 100, 100); + @property private _alignLeft = 0; + @property private _alignRight = 0; + @property private _alignCenter = 0; + @property private _alignTop = 0; + @property private _alignBottom = 0; + @property private _alignMiddle = 0; + @property private _horizontalAlignment = HorizontalAlignmentMode.None; + @property private _verticalAlignment = VerticalAlignmentMode.None; /** @@ -286,13 +283,11 @@ export class UITransform extends Transform { !this._horizontalAlignment && !this._verticalAlignment && super._onWorldMatrixChanging(); } - @ignoreClone protected override _onPositionChanged(): void { (this._horizontalAlignment || this._verticalAlignment) && this._updatePositionByAlignment(); super._onPositionChanged(); } - @ignoreClone protected override _onWorldPositionChanged(): void { super._onWorldPositionChanged(); if (this._horizontalAlignment || this._verticalAlignment) { @@ -365,7 +360,6 @@ export class UITransform extends Transform { this._rect.set(x, y, size.x, size.y); } - @ignoreClone private _onSizeChanged(): void { if ( this._horizontalAlignment === HorizontalAlignmentMode.LeftAndRight || @@ -379,7 +373,6 @@ export class UITransform extends Transform { this._entity._updateFlagManager.dispatch(UITransformModifyFlags.Size); } - @ignoreClone private _onPivotChanged(): void { this._updateRectBySizeAndPivot(); this._updateWorldFlagWithSelfRectChange(); diff --git a/packages/ui/src/component/advanced/Button.ts b/packages/ui/src/component/advanced/Button.ts index 73bf817562..e6f4748946 100644 --- a/packages/ui/src/component/advanced/Button.ts +++ b/packages/ui/src/component/advanced/Button.ts @@ -1,9 +1,9 @@ -import { deepClone, PointerEventData, Signal } from "@galacean/engine"; +import { PointerEventData, Signal, property } from "@galacean/engine"; import { UIInteractive } from "../interactive/UIInteractive"; export class Button extends UIInteractive { /** Signal emitted when the button is clicked. */ - @deepClone + @property readonly onClick = new Signal<[PointerEventData]>(); override onPointerClick(event: PointerEventData): void { diff --git a/packages/ui/src/component/advanced/Image.ts b/packages/ui/src/component/advanced/Image.ts index b364a2e303..6704aecf6d 100644 --- a/packages/ui/src/component/advanced/Image.ts +++ b/packages/ui/src/component/advanced/Image.ts @@ -12,8 +12,7 @@ import { SpriteModifyFlags, SpriteTileMode, TiledSpriteAssembler, - assignmentClone, - ignoreClone + property } from "@galacean/engine"; import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; import { RootCanvasModifyFlags } from "../UICanvas"; @@ -24,15 +23,12 @@ import { UITransform, UITransformModifyFlags } from "../UITransform"; * UI element that renders an image. */ export class Image extends UIRenderer implements ISpriteRenderer { - @ignoreClone private _sprite: Sprite = null; - @ignoreClone private _drawMode: SpriteDrawMode; - @ignoreClone private _assembler: ISpriteAssembler; - @assignmentClone + @property private _tileMode: SpriteTileMode = SpriteTileMode.Continuous; - @assignmentClone + @property private _tiledAdaptiveThreshold: number = 0.5; /** @@ -245,7 +241,6 @@ export class Image extends UIRenderer implements ISpriteRenderer { canvas._renderElements.push(renderElement); } - @ignoreClone protected override _onTransformChanged(type: number): void { if (type & UITransformModifyFlags.Size && this._drawMode === SpriteDrawMode.Tiled) { this._dirtyUpdateFlag |= ImageUpdateFlags.All; @@ -264,7 +259,6 @@ export class Image extends UIRenderer implements ISpriteRenderer { super._onDestroy(); } - @ignoreClone private _onSpriteChange(type: SpriteModifyFlags): void { switch (type) { case SpriteModifyFlags.texture: diff --git a/packages/ui/src/component/advanced/Text.ts b/packages/ui/src/component/advanced/Text.ts index b534253a9a..f263903d83 100644 --- a/packages/ui/src/component/advanced/Text.ts +++ b/packages/ui/src/component/advanced/Text.ts @@ -17,8 +17,7 @@ import { TextVerticalAlignment, Texture2D, Vector3, - assignmentClone, - ignoreClone + property } from "@galacean/engine"; import { CanvasRenderMode } from "../../enums/CanvasRenderMode"; import { RootCanvasModifyFlags } from "../UICanvas"; @@ -33,31 +32,29 @@ export class Text extends UIRenderer implements ITextRenderer { private static _worldPositions = [new Vector3(), new Vector3(), new Vector3(), new Vector3()]; private static _charRenderInfos: CharRenderInfo[] = []; - @ignoreClone private _textChunks = Array(); - @ignoreClone + @property private _subFont: SubFont = null; - @assignmentClone + @property private _text: string = ""; - @ignoreClone private _localBounds: BoundingBox = new BoundingBox(); - @assignmentClone + @property private _font: Font = null; - @assignmentClone + @property private _fontSize: number = 24; - @assignmentClone + @property private _fontStyle: FontStyle = FontStyle.None; - @assignmentClone + @property private _lineSpacing: number = 0; - @assignmentClone + @property private _characterSpacing: number = 0; - @assignmentClone + @property private _horizontalAlignment: TextHorizontalAlignment = TextHorizontalAlignment.Center; - @assignmentClone + @property private _verticalAlignment: TextVerticalAlignment = TextVerticalAlignment.Center; - @assignmentClone + @property private _enableWrapping: boolean = false; - @assignmentClone + @property private _overflowMode: OverflowMode = OverflowMode.Overflow; /** @@ -265,14 +262,6 @@ export class Text extends UIRenderer implements ITextRenderer { this._subFont && (this._subFont = null); } - // @ts-ignore - override _cloneTo(target: Text): void { - // @ts-ignore - super._cloneTo(target); - target.font = this._font; - target._subFont = this._subFont; - } - /** * @internal */ @@ -602,7 +591,6 @@ export class Text extends UIRenderer implements ITextRenderer { charRenderInfos.length = 0; } - @ignoreClone protected override _onTransformChanged(type: number): void { if (type & UITransformModifyFlags.Size || type & UITransformModifyFlags.Pivot) { this._dirtyUpdateFlag |= DirtyFlag.LocalPositionBounds; diff --git a/packages/ui/src/component/interactive/UIInteractive.ts b/packages/ui/src/component/interactive/UIInteractive.ts index 6811819585..f7bd6648e7 100644 --- a/packages/ui/src/component/interactive/UIInteractive.ts +++ b/packages/ui/src/component/interactive/UIInteractive.ts @@ -1,12 +1,4 @@ -import { - CloneUtils, - Entity, - EntityModifyFlags, - Script, - assignmentClone, - deepClone, - ignoreClone -} from "@galacean/engine"; +import { Entity, EntityModifyFlags, Script, property } from "@galacean/engine"; import { UIGroup } from "../.."; import { Utils } from "../../Utils"; import { IGroupAble } from "../../interface/IGroupAble"; @@ -21,44 +13,33 @@ export class UIInteractive extends Script implements IGroupAble { /** @internal */ _rootCanvas: UICanvas; /** @internal */ - @ignoreClone _indexInRootCanvas: number = -1; /** @internal */ - @ignoreClone _isRootCanvasDirty: boolean = false; /** @internal */ - @ignoreClone _rootCanvasListeningEntities: Entity[] = []; /** @internal */ _group: UIGroup; /** @internal */ - @ignoreClone _indexInGroup: number = -1; /** @internal */ - @ignoreClone _isGroupDirty: boolean = false; /** @internal */ - @ignoreClone _groupListeningEntities: Entity[] = []; /** @internal */ - @ignoreClone _globalInteractive: boolean = false; /** @internal */ - @ignoreClone _globalInteractiveDirty: boolean = false; - @deepClone + @property protected _transitions: Transition[] = []; - @assignmentClone + @property protected _interactive: boolean = true; - @ignoreClone protected _state: InteractiveState = InteractiveState.Normal; /** @todo Multi-touch points are not considered yet. */ - @ignoreClone private _isPointerInside: boolean = false; - @ignoreClone private _isPointerDragging: boolean = false; /** @@ -207,7 +188,6 @@ export class UIInteractive extends Script implements IGroupAble { /** * @internal */ - @ignoreClone _groupListener(flag: number): void { if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.GroupEnableInScene) { Utils.setGroupDirty(this); @@ -217,7 +197,6 @@ export class UIInteractive extends Script implements IGroupAble { /** * @internal */ - @ignoreClone _rootCanvasListener(flag: number): void { if (flag === EntityModifyFlags.Parent || flag === EntityUIModifyFlags.CanvasEnableInScene) { Utils.setRootCanvasDirty(this); @@ -228,7 +207,6 @@ export class UIInteractive extends Script implements IGroupAble { /** * @internal */ - @ignoreClone _onGroupModify(flags: GroupModifyFlags): void { if (flags & GroupModifyFlags.GlobalInteractive) { this._globalInteractiveDirty = true; diff --git a/packages/ui/src/component/interactive/transition/Transition.ts b/packages/ui/src/component/interactive/transition/Transition.ts index 5dd461e615..9728194219 100644 --- a/packages/ui/src/component/interactive/transition/Transition.ts +++ b/packages/ui/src/component/interactive/transition/Transition.ts @@ -1,4 +1,4 @@ -import { Color, ReferResource, Sprite } from "@galacean/engine"; +import { Color, ReferResource, Sprite, property } from "@galacean/engine"; import { UIRenderer } from "../../UIRenderer"; import { InteractiveState, UIInteractive } from "../UIInteractive"; @@ -10,13 +10,20 @@ export abstract class Transition< K extends UIRenderer = UIRenderer > { /** @internal */ + @property _interactive: UIInteractive; + @property protected _target: K; + @property protected _normal: T; + @property protected _pressed: T; + @property protected _hover: T; + @property protected _disabled: T; + @property protected _duration: number = 0; protected _countDown: number = 0; protected _initialValue: T; diff --git a/tests/src/core/2d/text/TextRenderer.test.ts b/tests/src/core/2d/text/TextRenderer.test.ts index e90aa4a06d..8b7a66d3f4 100644 --- a/tests/src/core/2d/text/TextRenderer.test.ts +++ b/tests/src/core/2d/text/TextRenderer.test.ts @@ -384,6 +384,29 @@ describe("TextRenderer", () => { expect(renderer2).not.to.be.undefined; }); + it("clone does not leak the default font ref count", () => { + const entity = rootEntity.createChild("TextRendererFontLeak"); + const renderer = entity.addComponent(TextRenderer); + const customFont = new Font(engine, "FontLeakTest"); + renderer.font = customFont; + + const defaultFont = engine["_textDefaultFont"] as Font; + const defaultBefore = defaultFont.refCount; + const customBefore = customFont.refCount; + + // The clone's constructor temporarily holds the default font; `_cloneTo` must release it and + // take a ref on the actual (custom) font instead — otherwise the default font leaks +1 per clone. + const clone = entity.clone(); + expect(defaultFont.refCount).to.equal(defaultBefore); + expect(customFont.refCount).to.equal(customBefore + 1); + + clone.destroy(); + expect(customFont.refCount).to.equal(customBefore); + expect(defaultFont.refCount).to.equal(defaultBefore); + + entity.destroy(); + }); + it("destroy", () => { // Test that destroy works correctly. let entity = rootEntity.findByName("TextRenderer2"); diff --git a/tests/src/core/Animator.test.ts b/tests/src/core/Animator.test.ts index 9544a7591e..d903ac2699 100644 --- a/tests/src/core/Animator.test.ts +++ b/tests/src/core/Animator.test.ts @@ -1815,4 +1815,23 @@ describe("Animator test", function () { // preserved by the playSpeed===0 guard expect(layerData.destPlayData?.playedTime).to.be.greaterThan(0); }); + + it("clone bumps AnimatorController ref count by exactly 1 (no leak)", () => { + const scene = engine.sceneManager.activeScene; + const entity = scene.createRootEntity("animator-refcount"); + const cloneAnimator = entity.addComponent(Animator); + const controller = new AnimatorController(engine); + cloneAnimator.animatorController = controller; + + const before = controller.refCount; + const clone = entity.clone(); + // Exactly +1: the clone gate's Assignment branch bumps the shared controller; the manual bump + // that used to also run in Animator._cloneTo would double-count (+2 -> leak on destroy). + expect(controller.refCount).to.eq(before + 1); + + clone.destroy(); + expect(controller.refCount).to.eq(before); + + entity.destroy(); + }); }); diff --git a/tests/src/core/Camera.test.ts b/tests/src/core/Camera.test.ts index 0892400f07..dd7cf4a723 100644 --- a/tests/src/core/Camera.test.ts +++ b/tests/src/core/Camera.test.ts @@ -429,6 +429,84 @@ describe("camera test", function () { expect(cloneCamera._globalShaderMacro).to.not.eq(camera._globalShaderMacro); }); + it("clone preserves custom view/projection matrices with their flags", () => { + const entity = rootEntity.createChild("customMatrixCameraSource"); + const cam = entity.addComponent(Camera); + const customView = new Matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3, 4, 5, 1); + const customProj = new Matrix(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1); + cam.viewMatrix = customView; + cam.projectionMatrix = customProj; + + const cloned = entity.clone(); + const clonedCam = cloned.getComponent(Camera); + + // The custom flag clones together with the matrix content (an independent copy, not a shared ref) + expect(clonedCam.viewMatrix).to.not.eq(cam.viewMatrix); + expect(Matrix.equals(clonedCam.viewMatrix as Matrix, customView)).to.eq(true); + expect(Matrix.equals(clonedCam.projectionMatrix as Matrix, customProj)).to.eq(true); + + // Custom matrices stay pinned on the clone: moving its transform must not trigger a recompute + cloned.transform.setPosition(10, 0, 0); + expect(Matrix.equals(clonedCam.viewMatrix as Matrix, customView)).to.eq(true); + + cloned.destroy(); + entity.destroy(); + }); + + it("clone preserves the replacement shader and keeps its ref count balanced", () => { + const entity = rootEntity.createChild("replacementCameraSource"); + const cam = entity.addComponent(Camera); + const shader = + Shader.find("CameraCloneReplacementTest") || + Shader.create("CameraCloneReplacementTest", [new ShaderPass("Default", [], [], ShaderLanguage.GLSLES100)]); + // @ts-ignore + const before = shader._getReferCount(); + + cam.setReplacementShader(shader, "ReplacementTag"); + // @ts-ignore — setReplacementShader now ref-counts the shader (managed like renderTarget) + expect(shader._getReferCount()).to.equal(before + 1); + // Re-setting the same shader leaves the count unchanged + cam.setReplacementShader(shader, "ReplacementTag"); + // @ts-ignore + expect(shader._getReferCount()).to.equal(before + 1); + + const cloned = entity.clone(); + const clonedCamera = cloned.getComponent(Camera); + // Clone shares the same shader (config carried over) and the gate bumps its ref count + // @ts-ignore + expect(clonedCamera._replacementShader).to.equal(shader); + // @ts-ignore + expect(clonedCamera._replacementSubShaderTag).to.equal(cam._replacementSubShaderTag); + // @ts-ignore + expect(shader._getReferCount()).to.equal(before + 2); + + cloned.destroy(); + // @ts-ignore + expect(shader._getReferCount()).to.equal(before + 1); + + // Replacing releases the old shader and acquires the new one + const shaderB = + Shader.find("CameraCloneReplacementTestB") || + Shader.create("CameraCloneReplacementTestB", [new ShaderPass("Default", [], [], ShaderLanguage.GLSLES100)]); + // @ts-ignore + const beforeB = shaderB._getReferCount(); + cam.setReplacementShader(shaderB, "ReplacementTag"); + // @ts-ignore + expect(shader._getReferCount()).to.equal(before); + // @ts-ignore + expect(shaderB._getReferCount()).to.equal(beforeB + 1); + + // Reset releases; destroy after reset must not release again + cam.resetReplacementShader(); + // @ts-ignore + expect(shaderB._getReferCount()).to.equal(beforeB); + entity.destroy(); + // @ts-ignore + expect(shader._getReferCount()).to.equal(before); + // @ts-ignore + expect(shaderB._getReferCount()).to.equal(beforeB); + }); + it("destroy test", () => { camera.destroy(); }); diff --git a/tests/src/core/CloneUtils.test.ts b/tests/src/core/Clone.test.ts similarity index 65% rename from tests/src/core/CloneUtils.test.ts rename to tests/src/core/Clone.test.ts index e583d6d8ab..6598afed4c 100644 --- a/tests/src/core/CloneUtils.test.ts +++ b/tests/src/core/Clone.test.ts @@ -1,93 +1,160 @@ -import { Entity, MeshRenderer, Script, Signal, assignmentClone, deepClone, ignoreClone } from "@galacean/engine-core"; +import { + Entity, + MeshRenderer, + ParticleCompositeCurve, + ParticleCompositeGradient, + Script, + Signal, + property, + ShaderMacro, + CloneManager, + CompareFunction, + StencilOperation, + CullMode, + BlendFactor, + BlendOperation, + ColorWriteMask, + PostProcessEffectFloatParameter +} from "@galacean/engine-core"; +import { DepthState } from "@galacean/engine-core/src/shader/state/DepthState"; +import { StencilState } from "@galacean/engine-core/src/shader/state/StencilState"; +import { RasterState } from "@galacean/engine-core/src/shader/state/RasterState"; +import { RenderTargetBlendState } from "@galacean/engine-core/src/shader/state/RenderTargetBlendState"; +import { Color } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; import { describe, expect, it } from "vitest"; class TestScript extends Script { + @property targetEntity: Entity; + @property targetRenderer: MeshRenderer; + @property externalEntity: Entity; + @property externalRenderer: MeshRenderer; + @property deepChild: Entity; + @property selfRef: Entity; + @property speed: number; + @property name2: string; + @property flag: boolean; + @property data: object; } /** Script with multiple entity/component refs pointing to different nodes */ class MultiRefScript extends Script { + @property entityA: Entity; + @property entityB: Entity; + @property rendererA: MeshRenderer; + @property rendererB: MeshRenderer; } /** Script where the same entity is referenced by multiple properties */ class DuplicateRefScript extends Script { + @property ref1: Entity; + @property ref2: Entity; } /** Script with null/undefined entity/component refs */ class NullRefScript extends Script { + @property nullEntity: Entity = null; + @property undefinedEntity: Entity; + @property nullRenderer: MeshRenderer = null; + @property someNumber: number = 0; } /** Script referencing a sibling entity (not parent/child, but sibling under clone root) */ class SiblingRefScript extends Script { + @property sibling: Entity; + @property siblingRenderer: MeshRenderer; } -/** Script with a mix of decorated and undecorated entity refs */ -class DecoratedRefScript extends Script { - // Undecorated — should auto-remap via _remap - autoRemapEntity: Entity; - - // @assignmentClone — should still auto-remap since _remap takes priority - @assignmentClone - assignedEntity: Entity; +/** Script mixing a @property (cloned) entity ref and an unmarked (not cloned) one */ +class MarkedRefScript extends Script { + // @property — cloned; being an Entity, it is remapped to the clone + @property + markedEntity: Entity; + // unmarked — not part of the property state, so it is not cloned (left at the clone's default) + unmarkedEntity: Entity; +} - // @ignoreClone — should still auto-remap since _remap takes priority - @ignoreClone - ignoredEntity: Entity; +/** Script mixing @property and unmarked primitive + entity fields */ +class PartialCloneScript extends Script { + @property + markedValue = 1; + unmarkedValue = 1; + @property + markedRef: Entity; + unmarkedRef: Entity; } -/** Script with a @deepClone array of entities */ +/** Script with a @property array of entities */ class ArrayRefScript extends Script { - @deepClone + @property entities: Entity[] = []; } /** Script with Component self-reference */ class SelfComponentRefScript extends Script { + @property selfScript: SelfComponentRefScript; } /** Script referencing another Script on a different entity */ class CrossScriptRefScript extends Script { + @property otherScript: TestScript; } /** Script with a nested plain object containing entity refs */ class NestedObjectScript extends Script { - @deepClone + @property config: { target: Entity; label: string } = { target: null, label: "" }; } +/** Script with a @property nested array of arrays containing entities */ +class NestedArrayScript extends Script { + @property + groups: Entity[][] = []; +} + +/** Script with a @property Map containing entity values */ +class MapRefScript extends Script { + @property + entityMap: Map = new Map(); +} + /** Script for testing multiple same-type components on one entity */ class CounterScript extends Script { + @property value: number = 0; + @property partner: CounterScript; + @property targetEntity: Entity; } /** Script that references a CounterScript */ class CounterRefScript extends Script { + @property counter: CounterScript; } @@ -108,17 +175,27 @@ class ClickHandler extends Script { /** Script with a Signal property */ class SignalScript extends Script { - @deepClone + @property readonly onFire = new Signal<[number]>(); } +/** Script holding bare @property value-type fields (no initializer → cloned via `new ctor()`) */ +class ValueTypeFieldScript extends Script { + @property + curve: ParticleCompositeCurve; + @property + gradient: ParticleCompositeGradient; +} + describe("Clone remap", async () => { - const engine = await WebGLEngine.create({ canvas: document.createElement("canvas") }); + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = 1024; + const engine = await WebGLEngine.create({ canvas }); const scene = engine.sceneManager.activeScene; engine.run(); describe("Basic Entity/Component remap", () => { - it("script undecorated Entity ref should remap to cloned entity", () => { + it("script @property Entity ref should remap to cloned entity", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const child = parent.createChild("child"); @@ -136,7 +213,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("script undecorated Component ref should remap to cloned component", () => { + it("script @property Component ref should remap to cloned component", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const child = parent.createChild("child"); @@ -236,7 +313,9 @@ describe("Clone remap", async () => { expect(clonedScript.speed).eq(42); expect(clonedScript.name2).eq("test"); expect(clonedScript.flag).eq(true); - expect(clonedScript.data).eq(obj); + // Plain objects are deep cloned (independent copy) + expect(clonedScript.data).not.eq(obj); + expect((clonedScript.data).x).eq(1); rootEntity.destroy(); }); @@ -332,72 +411,78 @@ describe("Clone remap", async () => { }); }); - describe("Clone decorator interaction with _remap", () => { - it("@assignmentClone entity ref still gets remapped via _remap priority", () => { + describe("Opt-in: marked vs unmarked fields", () => { + it("@property entity ref remaps to the cloned entity", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.assignedEntity = child; + const script = parent.addComponent(MarkedRefScript); + script.markedEntity = child; const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); + const cs = cloned.getComponent(MarkedRefScript); - expect(cs.assignedEntity).not.eq(child); - expect(cs.assignedEntity).eq(cloned.children[0]); + expect(cs.markedEntity).not.eq(child); + expect(cs.markedEntity).eq(cloned.children[0]); rootEntity.destroy(); }); - it("@ignoreClone entity ref still gets remapped via _remap priority", () => { + it("unmarked entity ref is not cloned (left at the clone's default)", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.ignoredEntity = child; + const script = parent.addComponent(MarkedRefScript); + script.unmarkedEntity = child; const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); + const cs = cloned.getComponent(MarkedRefScript); - expect(cs.ignoredEntity).not.eq(child); - expect(cs.ignoredEntity).eq(cloned.children[0]); + // Opt-in: a field without @property is not part of the cloned state. + expect(cs.unmarkedEntity).eq(undefined); rootEntity.destroy(); }); - it("undecorated entity ref remaps correctly", () => { + it("unmarked entity ref outside hierarchy is also not cloned", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); - const child = parent.createChild("child"); - const script = parent.addComponent(DecoratedRefScript); - script.autoRemapEntity = child; + const external = rootEntity.createChild("external"); + const script = parent.addComponent(MarkedRefScript); + script.unmarkedEntity = external; const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); + const cs = cloned.getComponent(MarkedRefScript); - expect(cs.autoRemapEntity).not.eq(child); - expect(cs.autoRemapEntity).eq(cloned.children[0]); + expect(cs.unmarkedEntity).eq(undefined); rootEntity.destroy(); }); - it("@ignoreClone entity ref outside hierarchy stays original", () => { + it("unmarked primitive is not cloned (stays at the constructor default)", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); - const external = rootEntity.createChild("external"); - const script = parent.addComponent(DecoratedRefScript); - script.ignoredEntity = external; + const child = parent.createChild("child"); + const script = parent.addComponent(PartialCloneScript); + script.markedValue = 42; + script.unmarkedValue = 42; + script.markedRef = child; + script.unmarkedRef = child; const cloned = parent.clone(); - const cs = cloned.getComponent(DecoratedRefScript); + const cs = cloned.getComponent(PartialCloneScript); - expect(cs.ignoredEntity).eq(external); + // marked → copied / remapped; unmarked → left at the constructor default. + expect(cs.markedValue).eq(42); + expect(cs.unmarkedValue).eq(1); + expect(cs.markedRef).eq(cloned.children[0]); + expect(cs.unmarkedRef).eq(undefined); rootEntity.destroy(); }); }); - describe("@deepClone array of entities", () => { + describe("@property array of entities", () => { it("deep cloned entity array should remap internal refs", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); @@ -503,7 +588,7 @@ describe("Clone remap", async () => { }); }); - describe("Nested @deepClone object with entity refs", () => { + describe("Nested @property object with entity refs", () => { it("entity ref inside deep cloned plain object should remap", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); @@ -540,7 +625,7 @@ describe("Clone remap", async () => { }); describe("Signal clone with structured bindings", () => { - it("@deepClone Signal should not copy closure listeners", () => { + it("@property Signal should not copy closure listeners", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const script = parent.addComponent(SignalScript); @@ -559,7 +644,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("@deepClone Signal should remap structured binding target to cloned hierarchy", () => { + it("@property Signal should remap structured binding target to cloned hierarchy", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const handlerEntity = parent.createChild("handler"); @@ -578,7 +663,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("@deepClone Signal should keep external structured binding target", () => { + it("@property Signal should keep external structured binding target", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const external = rootEntity.createChild("external"); @@ -595,7 +680,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("@deepClone Signal should remap structured binding with pre-resolved args", () => { + it("@property Signal should remap structured binding with pre-resolved args", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const handlerEntity = parent.createChild("handler"); @@ -615,7 +700,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("@deepClone Signal should preserve once flag on structured binding", () => { + it("@property Signal should preserve once flag on structured binding", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const handlerEntity = parent.createChild("handler"); @@ -836,7 +921,7 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); - it("@deepClone array referencing specific component among same-type siblings", () => { + it("@property array referencing specific component among same-type siblings", () => { const rootEntity = scene.createRootEntity("root"); const parent = rootEntity.createChild("parent"); const child = parent.createChild("child"); @@ -867,4 +952,234 @@ describe("Clone remap", async () => { rootEntity.destroy(); }); }); + + describe("@property nested array of arrays with entity refs", () => { + it("@property nested entity arrays recursively clone and remap", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const childA = parent.createChild("childA"); + const childB = parent.createChild("childB"); + const childC = parent.createChild("childC"); + const script = parent.addComponent(NestedArrayScript); + script.groups = [[childA, childB], [childC]]; + + const cloned = parent.clone(); + const cs = cloned.getComponent(NestedArrayScript); + + expect(cs.groups).not.eq(script.groups); + expect(cs.groups.length).eq(2); + expect(cs.groups[0]).not.eq(script.groups[0]); + expect(cs.groups[1]).not.eq(script.groups[1]); + expect(cs.groups[0][0]).eq(cloned.children[0]); + expect(cs.groups[0][1]).eq(cloned.children[1]); + expect(cs.groups[1][0]).eq(cloned.children[2]); + + rootEntity.destroy(); + }); + }); + + describe("@property Map with entity values", () => { + it("@property Map creates new Map and remaps entity values", () => { + const rootEntity = scene.createRootEntity("root"); + const parent = rootEntity.createChild("parent"); + const child = parent.createChild("child"); + const external = rootEntity.createChild("external"); + const script = parent.addComponent(MapRefScript); + script.entityMap.set("internal", child); + script.entityMap.set("external", external); + + const cloned = parent.clone(); + const cs = cloned.getComponent(MapRefScript); + + expect(cs.entityMap).not.eq(script.entityMap); + expect(cs.entityMap.size).eq(2); + expect(cs.entityMap.get("internal")).eq(cloned.children[0]); + expect(cs.entityMap.get("external")).eq(external); + + rootEntity.destroy(); + }); + }); + + describe("ShaderData clone", () => { + it("component shaderData macros and values survive clone", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("renderer"); + const renderer = entity.addComponent(MeshRenderer); + renderer.shaderData.enableMacro("CUSTOM_CLONE_MACRO"); + renderer.shaderData.setFloat("u_customCloneValue", 7); + + const cloned = entity.clone(); + const clonedRenderer = cloned.getComponent(MeshRenderer); + + // The clone gets its own ShaderData instance, but with the source's macros + values copied in + // (not left at the constructor defaults of a freshly-built ShaderData). + expect(clonedRenderer.shaderData).not.eq(renderer.shaderData); + // @ts-ignore — macro carried over to the clone + expect(clonedRenderer.shaderData._macroCollection.isEnable(ShaderMacro.getByName("CUSTOM_CLONE_MACRO"))).eq(true); + expect(clonedRenderer.shaderData.getFloat("u_customCloneValue")).eq(7); + + cloned.destroy(); + rootEntity.destroy(); + }); + + it("cloned renderer preserves receiveShadows macro state", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("renderer"); + const renderer = entity.addComponent(MeshRenderer); + // Disabling receiveShadows clears the macro on shaderData; the constructor enables it by default, + // so a clone that doesn't copy shaderData would keep the macro enabled while the field reads false. + renderer.receiveShadows = false; + + const cloned = entity.clone(); + const clonedRenderer = cloned.getComponent(MeshRenderer); + const macro = ShaderMacro.getByName("RENDERER_IS_RECEIVE_SHADOWS"); + + expect(clonedRenderer.receiveShadows).eq(false); + // @ts-ignore — the derived macro must match the field, not the ctor default (enabled) + expect(clonedRenderer.shaderData._macroCollection.isEnable(macro)).eq(false); + + cloned.destroy(); + rootEntity.destroy(); + }); + }); + + describe("Deep-clone parameterless construct", () => { + it("a bare @property value-type field clones via its parameterless constructor", () => { + const rootEntity = scene.createRootEntity("root"); + const entity = rootEntity.createChild("valueTypes"); + const script = entity.addComponent(ValueTypeFieldScript); + script.curve = new ParticleCompositeCurve(5); + script.gradient = new ParticleCompositeGradient(new Color(1, 0, 0, 1)); + + // The fields have no initializer, so the clone target's slots are undefined — the gate has no + // reuse and must build each value with `new ctor()`. That requires these engine value types to + // be parameterless-constructible (state is then restored by the Populate stage). A value type + // that demanded constructor args here (the pre-fix ParticleCompositeGradient) would throw. + const cloned = entity.clone(); + const clonedScript = cloned.getComponent(ValueTypeFieldScript); + + expect(clonedScript.curve).to.not.eq(script.curve); + expect(clonedScript.curve.constant).to.eq(5); + expect(clonedScript.gradient).to.not.eq(script.gradient); + expect(clonedScript.gradient.constant.r).to.eq(1); + + // Independent copies — mutating the clone must not leak back into the source. + clonedScript.curve.constant = 9; + expect(script.curve.constant).to.eq(5); + + rootEntity.destroy(); + }); + }); + + describe("RenderState clone", () => { + it("DepthState fields should be cloned via @property", () => { + const source = new DepthState(); + source.enabled = false; + source.compareFunction = CompareFunction.Always; + source.writeEnabled = false; + + const cloneMap = new Map(); + const target = new DepthState(); + CloneManager.getProperties(DepthState).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target.enabled).to.eq(false); + expect(target.compareFunction).to.eq(CompareFunction.Always); + expect(target.writeEnabled).to.eq(false); + }); + + it("StencilState fields should be cloned via @property", () => { + const source = new StencilState(); + source.enabled = true; + source.referenceValue = 42; + source.mask = 0xf0; + source.compareFunctionFront = CompareFunction.NotEqual; + source.passOperationBack = StencilOperation.Replace; + + const cloneMap = new Map(); + const target = new StencilState(); + CloneManager.getProperties(StencilState).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target.enabled).to.eq(true); + expect(target.referenceValue).to.eq(42); + expect(target.mask).to.eq(0xf0); + expect(target.compareFunctionFront).to.eq(CompareFunction.NotEqual); + expect(target.passOperationBack).to.eq(StencilOperation.Replace); + }); + + it("RasterState fields should be cloned via @property", () => { + const source = new RasterState(); + source.cullMode = CullMode.Front; + source.depthBias = 0.5; + source.slopeScaledDepthBias = 1.5; + + const cloneMap = new Map(); + const target = new RasterState(); + CloneManager.getProperties(RasterState).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target.cullMode).to.eq(CullMode.Front); + expect(target.depthBias).to.eq(0.5); + expect(target.slopeScaledDepthBias).to.eq(1.5); + }); + + it("RenderTargetBlendState fields should be cloned via @property", () => { + const source = new RenderTargetBlendState(); + source.enabled = true; + source.colorBlendOperation = BlendOperation.Subtract; + source.alphaBlendOperation = BlendOperation.Max; + source.sourceColorBlendFactor = BlendFactor.SourceAlpha; + source.sourceAlphaBlendFactor = BlendFactor.One; + source.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + source.destinationAlphaBlendFactor = BlendFactor.Zero; + source.colorWriteMask = ColorWriteMask.Red | ColorWriteMask.Green; + + const cloneMap = new Map(); + const target = new RenderTargetBlendState(); + CloneManager.getProperties(RenderTargetBlendState).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target.enabled).to.eq(true); + expect(target.colorBlendOperation).to.eq(BlendOperation.Subtract); + expect(target.alphaBlendOperation).to.eq(BlendOperation.Max); + expect(target.sourceColorBlendFactor).to.eq(BlendFactor.SourceAlpha); + expect(target.sourceAlphaBlendFactor).to.eq(BlendFactor.One); + expect(target.destinationColorBlendFactor).to.eq(BlendFactor.OneMinusSourceAlpha); + expect(target.destinationAlphaBlendFactor).to.eq(BlendFactor.Zero); + expect(target.colorWriteMask).to.eq(ColorWriteMask.Red | ColorWriteMask.Green); + }); + }); + + describe("PostProcessEffectParameter clone", () => { + it("_needLerp should be preserved after clone", () => { + const source = new PostProcessEffectFloatParameter(0.5, 0, 1, true); + expect(source._needLerp).to.eq(true); + + const cloneMap = new Map(); + const target = new PostProcessEffectFloatParameter(0, 0, 1, false); + CloneManager.getProperties(source.constructor).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target._needLerp).to.eq(true); + expect(target.value).to.eq(0.5); + }); + + it("_needLerp=false should also be preserved", () => { + const source = new PostProcessEffectFloatParameter(1, 0, 10, false); + const target = new PostProcessEffectFloatParameter(0, 0, 10, true); + + const cloneMap = new Map(); + CloneManager.getProperties(source.constructor).forEach((k) => { + CloneManager.copyProperty(source, target, k, cloneMap); + }); + + expect(target._needLerp).to.eq(false); + }); + }); }); diff --git a/tests/src/core/Signal.test.ts b/tests/src/core/Signal.test.ts index cbf399a731..30b730e458 100644 --- a/tests/src/core/Signal.test.ts +++ b/tests/src/core/Signal.test.ts @@ -283,9 +283,8 @@ describe("Signal", async () => { signal.on(fn); const srcRoot = root.createChild("clSrc1"); - const targetRoot = srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, new Map()); // Closure listeners should NOT be copied to clone targetSignal.invoke(42); @@ -305,10 +304,10 @@ describe("Signal", async () => { signal.on(handler, "handleClick"); const targetRoot = srcRoot.clone(); + const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, new Map([[handler, clonedHandler]])); - const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); expect(clonedHandler.callCount).toBe(1); @@ -327,9 +326,9 @@ describe("Signal", async () => { const srcRoot = root.createChild("clSrc3"); signal.on(externalHandler, "handleClick"); - const targetRoot = srcRoot.clone(); + srcRoot.clone(); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, new Map()); targetSignal.invoke(); expect(externalHandler.callCount).toBe(1); @@ -351,10 +350,17 @@ describe("Signal", async () => { signal.on(handler, "handleClickWithPrefix", refHandler); const targetRoot = srcRoot.clone(); + const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); + const clonedRef = targetRoot.findByName("ref").getComponent(TestHandler); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo( + targetSignal, + new Map([ + [handler, clonedHandler], + [refHandler, clonedRef] + ]) + ); - const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); expect(clonedHandler.callCount).toBe(1); @@ -374,10 +380,10 @@ describe("Signal", async () => { signal.once(handler, "handleClick"); const targetRoot = srcRoot.clone(); + const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); // @ts-ignore - signal._cloneTo(targetSignal, srcRoot, targetRoot); + signal._cloneTo(targetSignal, new Map([[handler, clonedHandler]])); - const clonedHandler = targetRoot.findByName("handler").getComponent(TestHandler); targetSignal.invoke(); targetSignal.invoke(); diff --git a/tests/src/core/Transform.test.ts b/tests/src/core/Transform.test.ts index eb9d66126e..847b469aad 100644 --- a/tests/src/core/Transform.test.ts +++ b/tests/src/core/Transform.test.ts @@ -1,4 +1,4 @@ -import { deepClone, Entity, Scene, Transform } from "@galacean/engine-core"; +import { property, Entity, Scene, Transform } from "@galacean/engine-core"; import { Vector2, Vector3 } from "@galacean/engine-math"; import { WebGLEngine } from "@galacean/engine"; import { beforeAll, describe, expect, it } from "vitest"; @@ -126,6 +126,6 @@ describe("Transform test", function () { }); class SubClassOfTransform extends Transform { - @deepClone + @property size: Vector2 = new Vector2(); } diff --git a/tests/src/core/particle/Burst.test.ts b/tests/src/core/particle/Burst.test.ts index 7765171e54..48915a7f43 100644 --- a/tests/src/core/particle/Burst.test.ts +++ b/tests/src/core/particle/Burst.test.ts @@ -67,6 +67,36 @@ describe("Burst", () => { expect(burst.repeatInterval).to.equal(0.1); }); + it("Bursts survive entity.clone() as deep, independent copies", () => { + const scene = engine.sceneManager.activeScene; + const root = scene.createRootEntity("BurstCloneRoot"); + const entity = root.createChild("src"); + const renderer = entity.addComponent(ParticleRenderer); + const emission = renderer.generator.emission; + emission.addBurst(new Burst(0.5, new ParticleCompositeCurve(10), 3, 0.2)); + + // Array members have no reuse slot — each Burst goes through `new Burst()` + field populate + const cloned = entity.clone(); + const srcBurst = emission.bursts[0]; + const clonedBursts = cloned.getComponent(ParticleRenderer).generator.emission.bursts; + + expect(clonedBursts.length).to.equal(1); + const clonedBurst = clonedBursts[0]; + expect(clonedBurst).to.not.equal(srcBurst); + expect(clonedBurst.time).to.equal(0.5); + expect(clonedBurst.cycles).to.equal(3); + expect(clonedBurst.repeatInterval).to.equal(0.2); + expect(clonedBurst.count).to.not.equal(srcBurst.count); + expect(clonedBurst.count.evaluate(undefined, undefined)).to.equal(10); + + // Mutating the clone must not leak back into the source + clonedBurst.count.constant = 99; + expect(srcBurst.count.evaluate(undefined, undefined)).to.equal(10); + + cloned.destroy(); + root.destroy(); + }); + it("Single cycle backward compatible", () => { const scene = engine.sceneManager.activeScene; const entity = scene.createRootEntity("SingleCycle"); diff --git a/tests/src/core/particle/ParticleShapeTransform.test.ts b/tests/src/core/particle/ParticleShapeTransform.test.ts index d3ac6d2ac6..c54af85ce7 100644 --- a/tests/src/core/particle/ParticleShapeTransform.test.ts +++ b/tests/src/core/particle/ParticleShapeTransform.test.ts @@ -196,10 +196,10 @@ describe("ParticleShapeTransform", function () { }); describe("Clone", function () { - // Simulate CloneManager: deepClone calls copyFrom, then _cloneTo + // Simulate CloneManager: deep clone calls copyFrom, then _cloneTo function simulateClone(source: BoxShape): BoxShape { const clone = new BoxShape(); - // @deepClone step: copyFrom on existing Vector3 (preserves constructor-bound callbacks) + // Populate step: copyFrom on existing Vector3 (preserves constructor-bound callbacks) clone.position.copyFrom(source.position); clone.rotation.copyFrom(source.rotation); clone.scale.copyFrom(source.scale); diff --git a/tests/src/core/physics/MeshColliderShape.test.ts b/tests/src/core/physics/MeshColliderShape.test.ts index 9506569526..ebebb5296a 100644 --- a/tests/src/core/physics/MeshColliderShape.test.ts +++ b/tests/src/core/physics/MeshColliderShape.test.ts @@ -763,5 +763,54 @@ describe("MeshColliderShape PhysX", () => { entity.destroy(); }); + + it("clone — cloned MeshColliderShape rebuilds its native PhysX shape", async () => { + // MeshColliderShape._nativeShape is not a cloned field; without a _cloneTo that cooks a fresh + // shape from the cloned vertex/index buffers, the cloned entity has no collision surface + // (a sphere falls straight through the cloned ground). + const groundEntity = root.createChild("meshGroundForClone"); + groundEntity.transform.setPosition(0, 0, 0); + const groundCollider = groundEntity.addComponent(StaticCollider); + const meshShape = new MeshColliderShape(); + const meshMaterial = meshShape.material; + const mesh = createModelMesh(engine, [-10, 0, -10, 10, 0, -10, -10, 0, 10, 10, 0, 10], [0, 2, 1, 1, 2, 3]); + meshShape.mesh = mesh; + groundCollider.addShape(meshShape); + + const clonedGround = groundEntity.clone(); + // Move the original aside so the cloned ground is the only surface below the sphere. + groundEntity.transform.setPosition(1000, 0, 0); + root.addChild(clonedGround); + clonedGround.transform.setPosition(0, 0, 0); + + const clonedShape = clonedGround.getComponent(StaticCollider).shapes[0] as MeshColliderShape; + // @ts-ignore — the cloned shape must have a usable native PhysX handle + expect(clonedShape._nativeShape).not.toBeNull(); + // @ts-ignore + expect(clonedShape._nativeShape._pxShape).toBeDefined(); + + const sphereEntity = root.createChild("sphereForClone"); + sphereEntity.transform.setPosition(0, 2, 0); + const dynamicCollider = sphereEntity.addComponent(DynamicCollider); + const sphereShape = new SphereColliderShape(); + const sphereMaterial = sphereShape.material; + sphereShape.radius = 0.5; + dynamicCollider.addShape(sphereShape); + + for (let i = 0; i < 60; i++) { + physicsScene._update(1 / 60); + } + + // Sphere lands on the cloned ground (y > -1) rather than falling forever (y < -10). + const sphereY = sphereEntity.transform.position.y; + expect(sphereY).toBeGreaterThan(-1); + expect(sphereY).toBeLessThan(2); + + groundEntity.destroy(); + clonedGround.destroy(); + sphereEntity.destroy(); + meshMaterial?.destroy(); + sphereMaterial?.destroy(); + }); }); }); diff --git a/tests/src/core/physics/PhysicsMaterial.test.ts b/tests/src/core/physics/PhysicsMaterial.test.ts index 676f917fe3..84036ea2dc 100644 --- a/tests/src/core/physics/PhysicsMaterial.test.ts +++ b/tests/src/core/physics/PhysicsMaterial.test.ts @@ -494,4 +494,58 @@ describe("PhysicsMaterial", () => { physicsMaterial.bounciness = 1; }).toThrowError(); }); + + it("cloned collider shape material keeps native values", () => { + // Clone deep-copies PhysicsMaterial's JS fields directly, bypassing the setters that push into + // the freshly-constructed native PxMaterial. Without a _cloneTo → _syncNative, the clone's native + // material stays at defaults (bounciness 0) while material.bounciness reads the source value. + const scene = engine.sceneManager.activeScene; + const originalGravity = scene.physics.gravity.clone(); + const originalFixedTimeStep = scene.physics.fixedTimeStep; + scene.physics.gravity = new Vector3(0, 0, 0); + scene.physics.fixedTimeStep = 1 / 60; + + try { + const wallEntity = addBox(new Vector3(1, 8, 8), StaticCollider, new Vector3(0, 0, 0)); + const wallMaterial = wallEntity.getComponent(StaticCollider).shapes[0].material; + wallMaterial.bounciness = 1; + wallMaterial.dynamicFriction = 0; + wallMaterial.staticFriction = 0; + wallMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + wallMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const sourceEntity = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0)); + const sourceCollider = sourceEntity.getComponent(DynamicCollider); + sourceCollider.linearDamping = 0; + sourceCollider.angularDamping = 0; + sourceCollider.automaticCenterOfMass = false; + sourceCollider.automaticInertiaTensor = false; + + const sourceMaterial = sourceCollider.shapes[0].material; + sourceMaterial.bounciness = 1; + sourceMaterial.dynamicFriction = 0; + sourceMaterial.staticFriction = 0; + sourceMaterial.bounceCombine = PhysicsMaterialCombineMode.Multiply; + sourceMaterial.frictionCombine = PhysicsMaterialCombineMode.Multiply; + + const cloneEntity = sourceEntity.clone(); + sourceEntity.destroy(); + rootEntity.addChild(cloneEntity); + cloneEntity.transform.setPosition(-3, 0, 0); + + const cloneCollider = cloneEntity.getComponent(DynamicCollider); + cloneCollider.linearVelocity = new Vector3(10, 0, 0); + + for (let i = 0; i < 40; i++) { + // @ts-ignore + scene.physics._update(scene.physics.fixedTimeStep); + } + + // A bouncy clone hitting the wall reverses its x velocity; a default (non-bouncy) native sticks. + expect(cloneCollider.linearVelocity.x).lessThan(-1); + } finally { + scene.physics.gravity = originalGravity; + scene.physics.fixedTimeStep = originalFixedTimeStep; + } + }); }); diff --git a/tests/src/loader/PrefabResource.test.ts b/tests/src/loader/PrefabResource.test.ts index 24088c2932..d9e9dabc9b 100644 --- a/tests/src/loader/PrefabResource.test.ts +++ b/tests/src/loader/PrefabResource.test.ts @@ -1,18 +1,22 @@ import { expect, beforeAll, afterAll, describe, it } from "vitest"; import { WebGLEngine } from "@galacean/engine"; -import { Loader, MeshRenderer, Script } from "@galacean/engine-core"; +import { Loader, MeshRenderer, Script, property } from "@galacean/engine-core"; import { PrefabParser } from "../../../packages/loader/src/prefab/PrefabParser"; import type { PrefabFile } from "../../../packages/loader/src/schema/PrefabSchema"; let engine: WebGLEngine; class DiceScript extends Script { + @property skinMesh: MeshRenderer = null; + @property numMesh: MeshRenderer = null; } class OverrideCallScript extends Script { + @property value = ""; + @property receivedArgs: string[] = []; appendSuffix(suffix: string): void { @@ -761,6 +765,7 @@ describe("Cross-prefab $component ref", () => { // We need a script holding an Entity ref — reuse OverrideCallScript? It doesn't have an Entity field. // Use DiceScript but expose a targetEntity through a separate Script below. class EntityRefScript extends Script { + @property target: Entity = null; } Loader.registerClass("EntityRefScript", EntityRefScript);