diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..98a0d79d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +# This is a basic workflow to help you get started with Actions + +name: Release Note + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "master" branch + release: + types: [published] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + message: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Dump Github context + env: + GITHUB_CONTEXT: ${{ toJSON(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Send notification to Dingding + if: success() + uses: mrkou47/action-dingding-webhook@main + env: + DINGDING_SECRET_TOKEN_MAP: ${{ secrets.DINGDING_SECRET_TOKEN_MAP }} + with: + title: Release Note ${{ github.event.release.tag_name }} + text: ${{ toJSON(github.event.release.body) }} + diff --git a/lerna.json b/lerna.json new file mode 100644 index 000000000..31c3f54ff --- /dev/null +++ b/lerna.json @@ -0,0 +1,12 @@ +{ + "npmClient": "npm", + "version": "0.8.0-beta.31", + "bootstrap": { + "hoist": true + }, + "packages": [ + "packages/*", + "tests/**" + ], + "loglevel": "verbose" +} diff --git a/packages/core/src/Component.ts b/packages/core/src/Component.ts index 9cc6fb820..197d51427 100644 --- a/packages/core/src/Component.ts +++ b/packages/core/src/Component.ts @@ -30,14 +30,17 @@ export abstract class Component extends EngineObject { } set enabled(value: boolean) { - if (value === this._enabled) { - return; - } - this._enabled = value; - if (value) { - this._entity.isActiveInHierarchy && this._onEnable(); - } else { - this._entity.isActiveInHierarchy && this._onDisable(); + if (value !== this._enabled) { + this._enabled = value; + if (this._entity.isActiveInHierarchy) { + if (value) { + this._phasedActive = true; + this._onEnable(); + } else { + this._phasedActive = false; + this._onDisable(); + } + } } } diff --git a/packages/core/src/asset/AssetType.ts b/packages/core/src/asset/AssetType.ts index 18ffbb6f4..fa4407135 100644 --- a/packages/core/src/asset/AssetType.ts +++ b/packages/core/src/asset/AssetType.ts @@ -25,8 +25,10 @@ export enum AssetType { Material = "material", /** Mesh. */ Mesh = "mesh", - /** Animation Clip. */ - AnimationClip = "animation-clip", + /** AnimationClip. */ + AnimationClip = "AnimationClip", + /** AnimatorController. */ + AnimatorController = "AnimatorController", /** Prefab.*/ Prefab = "prefab", /** Compress Texture. */ diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index 653683a5b..56f7c3f02 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -86,6 +86,7 @@ export class PointerManager implements IInput { this._upList.length = 0; this._movingDelta.set(0, 0); this._nativeEvents.length > 0 && this._handlePointerEvent(this._nativeEvents, frameCount); + this._pointers.length > 0 && (this._lastPositionFrameCount = frameCount); if (this._engine.physicsManager._initialized) { const rayCastEntity = this._pointerRayCast(); const { _keyEventCount: keyEventCount } = this; @@ -309,7 +310,6 @@ export class PointerManager implements IInput { if (this._lastPositionFrameCount === frameCount - 1) { this._movingDelta.set(currentPosition.x - lastX, currentPosition.y - lastY); } - this._lastPositionFrameCount = frameCount; } nativeEvents.length = 0; } diff --git a/packages/core/src/physics/DynamicCollider.ts b/packages/core/src/physics/DynamicCollider.ts index 0ed76387e..eef0222b6 100644 --- a/packages/core/src/physics/DynamicCollider.ts +++ b/packages/core/src/physics/DynamicCollider.ts @@ -272,8 +272,6 @@ export class DynamicCollider extends Collider { const { transform } = this.entity; const { worldPosition, worldRotationQuaternion } = transform; (this._nativeCollider).getWorldTransform(worldPosition, worldRotationQuaternion); - transform.worldPosition = worldPosition; - transform.worldRotationQuaternion = worldRotationQuaternion; this._updateFlag.flag = false; } } diff --git a/packages/loader/src/AnimatorControllerLoader.ts b/packages/loader/src/AnimatorControllerLoader.ts new file mode 100644 index 000000000..0925eaa65 --- /dev/null +++ b/packages/loader/src/AnimatorControllerLoader.ts @@ -0,0 +1,87 @@ +import { + resourceLoader, + Loader, + AssetPromise, + AssetType, + LoadItem, + ResourceManager, + AnimatorController, + AnimatorControllerLayer, + AnimatorStateMachine, + AnimatorStateTransition +} from "@oasis-engine/core"; + +@resourceLoader(AssetType.AnimatorController, ["json"], false) +class AnimatorControllerLoader extends Loader { + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject) => { + this.request(item.url, { + ...item, + type: "json" + }).then(async (data) => { + const animatorController = new AnimatorController(); + const { layers } = data; + const promises = []; + layers.forEach((layerData, layerIndex: number) => { + const { name, blendingMode, weight, stateMachine: stateMachineData } = layerData; + const layer = new AnimatorControllerLayer(name); + layer.blendingMode = blendingMode; + layer.weight = weight; + if (stateMachineData) { + const { states } = stateMachineData; + const stateMachine = (layer.stateMachine = new AnimatorStateMachine()); + states.forEach((stateData, stateIndex: number) => { + const { + name, + speed, + wrapMode, + clipStartNormalizedTime, + clipEndNormalizedTime, + isDefaultState, + clip: clipData + } = stateData; + const state = stateMachine.addState(name); + state.speed = speed; + state.wrapMode = wrapMode; + state.clipStartTime = clipStartNormalizedTime; + state.clipEndTime = clipEndNormalizedTime; + if (clipData) { + promises.push( + new Promise(async (resolve) => { + resolve({ + layerIndex, + stateIndex, + clip: await resourceManager.getResourceByRef(clipData) + }); + }) + ); + } + }); + states.forEach((stateData) => { + const { name, transitions } = stateData; + transitions.forEach((transitionData) => { + const { targetStateName, duration, offset, exitTime } = transitionData; + const sourceState = stateMachine.findStateByName(name); + const destState = stateMachine.findStateByName(targetStateName); + const transition = new AnimatorStateTransition(); + transition.destinationState = destState; + transition.duration = duration; + transition.exitTime = exitTime; + transition.offset = offset; + sourceState.addTransition(transition); + }); + }); + } + animatorController.addLayer(layer); + }); + Promise.all(promises).then((clipData) => { + clipData.forEach((data) => { + const { layerIndex, stateIndex, clip } = data; + animatorController.layers[layerIndex].stateMachine.states[stateIndex].clip = clip; + }); + resolve(animatorController); + }); + }); + }); + } +} diff --git a/packages/loader/src/MaterialLoader.ts b/packages/loader/src/MaterialLoader.ts index 60d1d026b..452836851 100644 --- a/packages/loader/src/MaterialLoader.ts +++ b/packages/loader/src/MaterialLoader.ts @@ -41,9 +41,10 @@ class MaterialLoader extends Loader { break; } - const materialShaderData: ShaderData = material.shaderData; + const texturePromises = new Array>(); + const materialShaderData = material.shaderData; for (let key in shaderData) { - const { type, value } = shaderData[key]; + const { type, value } = shaderData[key];`` switch (type) { case "Vector2": @@ -62,9 +63,11 @@ class MaterialLoader extends Loader { materialShaderData.setFloat(key, value); break; case "Texture": - resourceManager.getResourceByRef(value).then((texture) => { - materialShaderData.setTexture(key, texture); - }); + texturePromises.push( + resourceManager.getResourceByRef(value).then((texture) => { + materialShaderData.setTexture(key, texture); + }) + ); break; } } @@ -82,7 +85,9 @@ class MaterialLoader extends Loader { materialShaderData[key] = renderState[key]; } - resolve(material); + Promise.all(texturePromises).then(() => { + resolve(material); + }); }); }); } diff --git a/packages/loader/src/gltf/GLTFUtil.ts b/packages/loader/src/gltf/GLTFUtil.ts index 9501e0cf3..8f6727193 100644 --- a/packages/loader/src/gltf/GLTFUtil.ts +++ b/packages/loader/src/gltf/GLTFUtil.ts @@ -273,9 +273,6 @@ export class GLTFUtil { return new Promise((resolve, reject) => { const blob = new window.Blob([imageBuffer], { type }); const img = new Image(); - img.src = URL.createObjectURL(blob); - - img.crossOrigin = "anonymous"; img.onerror = function () { reject(new Error("Failed to load image buffer")); }; @@ -288,6 +285,8 @@ export class GLTFUtil { img.onabort = null; }); }; + img.crossOrigin = "anonymous"; + img.src = URL.createObjectURL(blob); }); } diff --git a/packages/loader/src/gltf/extensions/OASIS_materials_remap.ts b/packages/loader/src/gltf/extensions/OASIS_materials_remap.ts new file mode 100644 index 000000000..3eff7c8f7 --- /dev/null +++ b/packages/loader/src/gltf/extensions/OASIS_materials_remap.ts @@ -0,0 +1,13 @@ +import { Material } from "@oasis-engine/core"; +import { GLTFResource } from "../GLTFResource"; +import { registerExtension } from "../parser/Parser"; +import { ExtensionParser } from "./ExtensionParser"; +import { IOasisMaterialRemap } from "./Schema"; + +@registerExtension("OASIS_materials_remap") +class OasisMaterialsRemap extends ExtensionParser { + createEngineResource(schema: IOasisMaterialRemap, context: GLTFResource): Promise { + const { engine } = context; + return engine.resourceManager.getResourceByRef(schema); + } +} diff --git a/packages/loader/src/gltf/extensions/Schema.ts b/packages/loader/src/gltf/extensions/Schema.ts index 59d67fd64..53793d546 100644 --- a/packages/loader/src/gltf/extensions/Schema.ts +++ b/packages/loader/src/gltf/extensions/Schema.ts @@ -153,6 +153,12 @@ export interface IKHRXmp_Node { packet: number; } +export interface IOasisMaterialRemap { + refId: string; + key?: string; + isClone?: boolean; +} + export type ExtensionSchema = | IKHRLightsPunctual_Light | IKHRDracoMeshCompression diff --git a/packages/loader/src/gltf/extensions/index.ts b/packages/loader/src/gltf/extensions/index.ts index 7ef0b8892..a9ee9e029 100644 --- a/packages/loader/src/gltf/extensions/index.ts +++ b/packages/loader/src/gltf/extensions/index.ts @@ -11,3 +11,4 @@ import "./KHR_materials_volume"; import "./KHR_mesh_quantization"; import "./KHR_texture_basisu"; import "./KHR_texture_transform"; +import "./OASIS_materials_remap"; diff --git a/packages/loader/src/gltf/parser/MaterialParser.ts b/packages/loader/src/gltf/parser/MaterialParser.ts index 802a9278b..461cbd663 100644 --- a/packages/loader/src/gltf/parser/MaterialParser.ts +++ b/packages/loader/src/gltf/parser/MaterialParser.ts @@ -41,7 +41,12 @@ export class MaterialParser extends Parser { name = "" } = gltf.materials[i]; - const { KHR_materials_unlit, KHR_materials_pbrSpecularGlossiness, KHR_materials_clearcoat } = extensions; + const { + KHR_materials_unlit, + KHR_materials_pbrSpecularGlossiness, + KHR_materials_clearcoat, + OASIS_materials_remap + } = extensions; let material: UnlitMaterial | PBRMaterial | PBRSpecularMaterial = null; @@ -133,6 +138,16 @@ export class MaterialParser extends Parser { } } + if (OASIS_materials_remap) { + context.gltf.extensions = context.gltf.extensions ?? {}; + context.gltf.extensions["OASIS_materials_remap"] = context.gltf.extensions["OASIS_materials_remap"] ?? {}; + context.gltf.extensions["OASIS_materials_remap"][i] = Parser.createEngineResource( + "OASIS_materials_remap", + OASIS_materials_remap, + context + ); + } + if (doubleSided) { material.renderFace = RenderFace.Double; } else { diff --git a/packages/loader/src/gltf/parser/SceneParser.ts b/packages/loader/src/gltf/parser/SceneParser.ts index c08eda2b0..c4fad40d0 100644 --- a/packages/loader/src/gltf/parser/SceneParser.ts +++ b/packages/loader/src/gltf/parser/SceneParser.ts @@ -26,7 +26,7 @@ export class SceneParser extends Parser { return SceneParser._defaultMaterial; } - parse(context: GLTFResource): void { + parse(context: GLTFResource): Promise { const { gltf: { nodes, cameras: gltfCameras }, entities @@ -34,6 +34,8 @@ export class SceneParser extends Parser { if (!nodes) return; + const promises = []; + for (let i = 0; i < nodes.length; i++) { const gltfNode = nodes[i]; const { camera: cameraID, mesh: meshID, extensions = {} } = gltfNode; @@ -45,7 +47,7 @@ export class SceneParser extends Parser { } if (meshID !== undefined) { - this._createRenderer(context, gltfNode, entity); + promises.push(this._createRenderer(context, gltfNode, entity)); } if (KHR_lights_punctual) { @@ -59,6 +61,9 @@ export class SceneParser extends Parser { if (context.defaultSceneRoot) { this._createAnimator(context); } + + context.gltf.extensions && delete context.gltf.extensions["OASIS_materials_remap"]; + return Promise.all(promises); } private _createCamera(context: GLTFResource, cameraSchema: ICamera, entity: Entity): void { @@ -101,7 +106,7 @@ export class SceneParser extends Parser { camera.enabled = false; } - private _createRenderer(context: GLTFResource, gltfNode: INode, entity: Entity): void { + private _createRenderer(context: GLTFResource, gltfNode: INode, entity: Entity): Promise { const { engine, gltf: { meshes: gltfMeshes }, @@ -114,6 +119,7 @@ export class SceneParser extends Parser { const gltfMeshPrimitives = glTFMesh.primitives; const blendShapeWeights = gltfNode.weights || glTFMesh.weights; + const promises = []; for (let i = 0; i < gltfMeshPrimitives.length; i++) { const mesh = meshes[meshID][i]; let renderer: MeshRenderer | SkinnedMeshRenderer; @@ -134,8 +140,17 @@ export class SceneParser extends Parser { } const materialIndex = gltfMeshPrimitives[i].material; - const material = materials?.[materialIndex] || SceneParser._getDefaultMaterial(engine); - renderer.setMaterial(material); + const remapMaterials = context.gltf.extensions && context.gltf.extensions["OASIS_materials_remap"]; + if (remapMaterials && remapMaterials[materialIndex]) { + promises.push( + remapMaterials[materialIndex].then((mtl) => { + renderer.setMaterial(mtl); + }) + ); + } else { + const material = materials?.[materialIndex] || SceneParser._getDefaultMaterial(engine); + renderer.setMaterial(material); + } const { extensions = {} } = gltfMeshPrimitives[i]; const { KHR_materials_variants } = extensions; @@ -143,6 +158,7 @@ export class SceneParser extends Parser { Parser.parseEngineResource("KHR_materials_variants", KHR_materials_variants, renderer, context); } } + return Promise.all(promises); } private _createAnimator(context: GLTFResource) { diff --git a/packages/loader/src/index.ts b/packages/loader/src/index.ts index 0c36bc75f..97659b444 100644 --- a/packages/loader/src/index.ts +++ b/packages/loader/src/index.ts @@ -1,3 +1,4 @@ +import "./AnimatorControllerLoader"; import "./BufferLoader"; import "./EnvLoader"; import "./gltf/extensions/index"; @@ -15,8 +16,8 @@ import "./TextureCubeLoader"; export { parseSingleKTX } from "./compressed-texture"; export { GLTFResource } from "./gltf/GLTFResource"; +export type { GLTFParams } from "./GLTFLoader"; export * from "./resource-deserialize"; export * from "./SceneLoader"; export type { Texture2DParams } from "./Texture2DLoader"; -export type { GLTFParams } from "./GLTFLoader"; diff --git a/packages/loader/src/resource-deserialize/index.ts b/packages/loader/src/resource-deserialize/index.ts index 239500d05..30f6c9ec2 100644 --- a/packages/loader/src/resource-deserialize/index.ts +++ b/packages/loader/src/resource-deserialize/index.ts @@ -8,7 +8,6 @@ export { Texture2DDecoder } from "./resources/texture2D/TextureDecoder"; export { ReflectionParser } from "./resources/prefab/ReflectionParser"; export { PrefabParser } from "./resources/prefab/PrefabParser"; export * from "./resources/animationClip/AnimationClipDecoder"; -export { AnimatorControllerDecoder } from "./resources/animatorController/AnimatorControllerDecoder"; export type { IModelMesh } from "./resources/mesh/IModelMesh"; /** diff --git a/packages/loader/src/resource-deserialize/resources/animatorController/AnimatorControllerDecoder.ts b/packages/loader/src/resource-deserialize/resources/animatorController/AnimatorControllerDecoder.ts deleted file mode 100644 index b4e68aa78..000000000 --- a/packages/loader/src/resource-deserialize/resources/animatorController/AnimatorControllerDecoder.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - Engine, - AnimatorController, - AnimatorControllerLayer, - AnimatorStateMachine, - AnimationClip, - AnimatorStateTransition, - AssetType -} from "@oasis-engine/core"; -import type { BufferReader } from "../../utils/BufferReader"; -import { decoder } from "../../utils/Decorator"; - -@decoder("AnimatorController") -export class AnimatorControllerDecoder { - public static decode(engine: Engine, bufferReader: BufferReader): Promise { - return new Promise(async (resolve) => { - const animatorController = new AnimatorController(); - const objectId = bufferReader.nextStr(); - const layersLen = bufferReader.nextUint16(); - const clipLoadPromises = []; - - for (let i = 0; i < layersLen; ++i) { - const name = bufferReader.nextStr(); - const layer = new AnimatorControllerLayer(name); - layer.blendingMode = bufferReader.nextUint8(); - layer.weight = bufferReader.nextFloat32(); - const stateMachine = new AnimatorStateMachine(); - const statesLen = bufferReader.nextUint16(); - for (let j = 0; j < statesLen; ++j) { - const stateName = bufferReader.nextStr(); - const state = stateMachine.addState(stateName); - state.speed = bufferReader.nextFloat32(); - state.wrapMode = bufferReader.nextUint8(); - const isDefaultState = bufferReader.nextUint8() ? true : false; - const clipStartNormalizedTime = bufferReader.nextFloat32(); - const clipEndNormalizedTime = bufferReader.nextFloat32(); - const clipPath = bufferReader.nextStr(); - const clipObjectId = bufferReader.nextStr(); - clipLoadPromises.push( - AnimatorControllerDecoder.loadAndSetClip(engine, clipPath, clipObjectId).then((clip) => { - state.clip = clip; - state.clipStartTime = clip.length * clipStartNormalizedTime; - state.clipEndTime = clip.length * clipEndNormalizedTime; - }) - ); - // @ts-ignore - isDefaultState && (stateMachine._defaultState = state); - - const transitionsLen = bufferReader.nextUint16(); - for (let k = 0; k < transitionsLen; ++k) { - const transition = new AnimatorStateTransition(); - transition.duration = bufferReader.nextFloat32(); - transition.offset = bufferReader.nextFloat32(); - transition.exitTime = bufferReader.nextFloat32(); - transition.exitTime = bufferReader.nextFloat32(); - transition.destinationState = stateMachine.findStateByName(bufferReader.nextStr()); - state.addTransition(transition); - } - } - layer.stateMachine = stateMachine; - animatorController.addLayer(layer); - } - - Promise.all(clipLoadPromises).then(() => { - // @ts-ignore - engine.resourceManager._objectPool[objectId] = animatorController; - resolve(animatorController); - }); - }); - } - - public static loadAndSetClip(engine: Engine, path: string, objectId: string): Promise { - // @ts-ignore - return Promise.resolve(engine.resourceManager._objectPool[objectId]); - // return new Promise((resolve) => { - // engine.resourceManager - // .load({ - // url: path, - // // @ts-ignore - // type: AssetType.Oasis - // }) - // .then(() => { - // // 从缓存池获取对象 - // // @ts-ignore - // resolve(engine.resourceManager._objectPool[objectId]); - // }); - // }); - } -} diff --git a/packages/loader/src/resource-deserialize/resources/animatorController/type.ts b/packages/loader/src/resource-deserialize/resources/animatorController/type.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/loader/src/resource-deserialize/resources/prefab/ReflectionParser.ts b/packages/loader/src/resource-deserialize/resources/prefab/ReflectionParser.ts index 94e6053c6..367332c95 100644 --- a/packages/loader/src/resource-deserialize/resources/prefab/ReflectionParser.ts +++ b/packages/loader/src/resource-deserialize/resources/prefab/ReflectionParser.ts @@ -19,7 +19,11 @@ export class ReflectionParser { for (let i = 0; i < entityConfig.components.length; i++) { const componentConfig = entityConfig.components[i]; const key = !componentConfig.refId ? componentConfig.class : componentConfig.refId; - const component = entity.addComponent(Loader.getClass(key)); + let component; + if (key === "Animator") { + component = entity.getComponent(Loader.getClass(key)); + } + component = component || entity.addComponent(Loader.getClass(key)); const promise = this.parsePropsAndMethods(component, componentConfig, engine); promises.push(promise); } @@ -59,7 +63,7 @@ export class ReflectionParser { ): Promise { if (Array.isArray(value)) { return Promise.all(value.map((item) => this.parseBasicType(item, engine, resourceManager))); - } else if (typeof value === "object") { + } else if (typeof value === "object" && value != null) { if (this._isClass(value)) { // 类对象 return this.parseClassObject(value, engine, resourceManager); @@ -123,10 +127,10 @@ export class ReflectionParser { } private static _isClass(value: any): value is IClassObject { - return "class" in value; + return value["class"] != undefined; } private static _isRef(value: any): value is IReferenceType { - return "refId" in value; + return value["refId"] != undefined; } } diff --git a/packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts b/packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts index 496060a66..0a555a8f0 100644 --- a/packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts +++ b/packages/loader/src/resource-deserialize/resources/texture2D/TextureDecoder.ts @@ -42,7 +42,6 @@ export class Texture2DDecoder { } else { const blob = new window.Blob([imagesData[0]]); const img = new Image(); - img.src = URL.createObjectURL(blob); img.onload = () => { texture2D.setImageSource(img); let completedCount = 0; @@ -58,14 +57,15 @@ export class Texture2DDecoder { for (let i = 1; i < mipCount; i++) { const blob = new window.Blob([imagesData[i]]); const img = new Image(); - img.src = URL.createObjectURL(blob); img.onload = () => { texture2D.setImageSource(img, i); onComplete(); }; + img.src = URL.createObjectURL(blob); } } }; + img.src = URL.createObjectURL(blob); } }); } diff --git a/tests/src/core/Script.test.ts b/tests/src/core/Script.test.ts index dd878dcb5..c787386f5 100644 --- a/tests/src/core/Script.test.ts +++ b/tests/src/core/Script.test.ts @@ -118,5 +118,43 @@ describe("Scene", () => { expect(childScript.onEnable).to.have.been.called.exactly(0); expect(childScript.onDisable).to.have.been.called.exactly(0); }); + + it("Entity isActive = true after script call enabled = false", () => { + class TestScript extends Script { + onAwake() { + console.log("TestScript___onAwake"); + } + onEnable() { + console.log("TestScript___onEnable"); + } + + onDisable() { + console.log("TestScript___onDisable"); + } + + onDestroy() { + console.log("TestScript___onDestroy"); + } + } + TestScript.prototype.onAwake = chai.spy(TestScript.prototype.onAwake); + TestScript.prototype.onEnable = chai.spy(TestScript.prototype.onEnable); + TestScript.prototype.onDisable = chai.spy(TestScript.prototype.onDisable); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const entity = rootEntity.createChild("entity"); + const script = entity.addComponent(TestScript); + + entity.isActive = false; + script.enabled = false; + entity.isActive = true; + script.enabled = true; + entity.isActive = false; + + expect(script.onAwake).to.have.been.called.exactly(1); + expect(script.onEnable).to.have.been.called.exactly(2); + expect(script.onDisable).to.have.been.called.exactly(2); + }); }); });