From 8e5a7baf5fc6e0aace8dc7034692da84935b5a04 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 14:37:08 +0300 Subject: [PATCH 01/14] allow components ordering --- src/core/sort.js | 17 ++++++++++ src/framework/components/component.js | 11 +++++++ .../components/rigid-body/component.js | 2 ++ src/framework/components/system.js | 4 +++ src/framework/entity.js | 31 ++++++++++++------- 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/core/sort.js b/src/core/sort.js index 062667ced3a..b03fcb70348 100644 --- a/src/core/sort.js +++ b/src/core/sort.js @@ -6,6 +6,15 @@ */ const cmpPriority = (a, b) => a.priority - b.priority; + +/** + * @param {{order: number}} a - First object with `order` property. + * @param {{order: number}} b - Second object with `order` property. + * @returns {number} A number indicating the relative position. + * @ignore + */ +const cmpOrder = (a, b) => a.order - b.order; + /** * @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains * an object with at least a priority property. @@ -13,3 +22,11 @@ const cmpPriority = (a, b) => a.priority - b.priority; * @ignore */ export const sortPriority = arr => arr.sort(cmpPriority); + +/** + * @param {Array<{order: number}>} arr - Array to be sorted in place where each element contains + * an object with at least an `order` property. + * @returns {Array<{order: number}>} In place sorted array. + * @ignore + */ +export const sortOrder = arr => arr.sort(cmpOrder); diff --git a/src/framework/components/component.js b/src/framework/components/component.js index ca3bee8685d..a337e595025 100644 --- a/src/framework/components/component.js +++ b/src/framework/components/component.js @@ -19,6 +19,9 @@ class Component extends EventHandler { */ entity; + /** @private */ + _order = 0; + /** * Base constructor for a Component. * @@ -122,6 +125,14 @@ class Component extends EventHandler { get enabled() { return true; } + + set order(newOrder) { + this._order = newOrder; + } + + get order() { + return this._order; + } } export { Component }; diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 2a35c63129e..20355869769 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -168,6 +168,8 @@ class RigidBodyComponent extends Component { */ constructor(system, entity) { // eslint-disable-line no-useless-constructor super(system, entity); + + this._order = -1; } /** @ignore */ diff --git a/src/framework/components/system.js b/src/framework/components/system.js index 506d6803235..2460c87400e 100644 --- a/src/framework/components/system.js +++ b/src/framework/components/system.js @@ -4,6 +4,7 @@ import { Color } from '../../core/math/color.js'; import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Vec4 } from '../../core/math/vec4.js'; +import { sortOrder } from '../../core/sort.js'; /** * Component Systems contain the logic and functionality to update all Components of a particular @@ -50,6 +51,9 @@ class ComponentSystem extends EventHandler { entity[this.id] = component; entity.c[this.id] = component; + entity.oc.push(component); + sortOrder(entity.oc); + this.initializeComponentData(component, data, []); this.fire('add', entity, component); diff --git a/src/framework/entity.js b/src/framework/entity.js index 08b80812789..4fec08c6d59 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -232,6 +232,9 @@ class Entity extends GraphNode { */ _guid = null; + /** @private */ + _oc = []; + /** * Used to differentiate between the entities of a template root instance, which have it set to * true, and the cloned instance entities (set to false). @@ -281,6 +284,15 @@ class Entity extends GraphNode { this._app = app; } + /** + * Component ordered storage. + * + * @returns {Array} - An ordered components array. + */ + get oc() { + return this._oc; + } + /** * Create a new component and add it to the entity. Use this to add functionality to the entity * like rendering a model, playing sounds and so on. @@ -503,17 +515,14 @@ class Entity extends GraphNode { _onHierarchyStateChanged(enabled) { super._onHierarchyStateChanged(enabled); - // enable / disable all the components - const components = this.c; - for (const type in components) { - if (components.hasOwnProperty(type)) { - const component = components[type]; - if (component.enabled) { - if (enabled) { - component.onEnable(); - } else { - component.onDisable(); - } + const components = this._oc; + for (let i = 0; i < components.length; i++) { + const component = components[i]; + if (component.enabled) { + if (enabled) { + component.onEnable(); + } else { + component.onDisable(); } } } From 0508a0211e3140d9dd03f4fce3f16b8e98584247 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 14:41:20 +0300 Subject: [PATCH 02/14] post changed order --- src/framework/entity.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/framework/entity.js b/src/framework/entity.js index 4fec08c6d59..b15f44d1272 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -531,10 +531,9 @@ class Entity extends GraphNode { /** @private */ _onHierarchyStatePostChanged() { // post enable all the components - const components = this.c; - for (const type in components) { - if (components.hasOwnProperty(type)) - components[type].onPostStateChange(); + const components = this._oc; + for (let i = 0; i < components.length; i++) { + components[i].onPostStateChange(); } } From 8843d0f7328b32850140e1f0e3907b3cb86aac0a Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 14:46:22 +0300 Subject: [PATCH 03/14] docs --- src/framework/components/component.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/framework/components/component.js b/src/framework/components/component.js index a337e595025..65a80e47fa0 100644 --- a/src/framework/components/component.js +++ b/src/framework/components/component.js @@ -126,6 +126,13 @@ class Component extends EventHandler { return true; } + /** + * Component order. When an entity with multiple components gets enabled, this order specifies + * in which order the components get enabled. The lowest number gets enabled first. By default, + * the components will be enabled in order they were added to an entity. + * + * @type {number} - Component order number. + */ set order(newOrder) { this._order = newOrder; } From 89b2b703fc799cff66cd9b6ad6b42a5042b7b125 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 14:56:59 +0300 Subject: [PATCH 04/14] make it private --- src/framework/components/component.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/framework/components/component.js b/src/framework/components/component.js index 65a80e47fa0..aeb87d8004d 100644 --- a/src/framework/components/component.js +++ b/src/framework/components/component.js @@ -19,7 +19,7 @@ class Component extends EventHandler { */ entity; - /** @private */ + /** @ignore */ _order = 0; /** @@ -132,6 +132,7 @@ class Component extends EventHandler { * the components will be enabled in order they were added to an entity. * * @type {number} - Component order number. + * @private */ set order(newOrder) { this._order = newOrder; From ae1dc3820ac954651ce57c439569124a6d7209ac Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 15:36:06 +0300 Subject: [PATCH 05/14] clear on remove --- src/framework/components/system.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/framework/components/system.js b/src/framework/components/system.js index 2460c87400e..8040bad50e3 100644 --- a/src/framework/components/system.js +++ b/src/framework/components/system.js @@ -71,15 +71,19 @@ class ComponentSystem extends EventHandler { * @ignore */ removeComponent(entity) { + const id = this.id; + const ordered = entity.oc; const record = this.store[entity.getGuid()]; - const component = entity.c[this.id]; + const component = entity.c[id]; this.fire('beforeremove', entity, component); delete this.store[entity.getGuid()]; - entity[this.id] = undefined; - delete entity.c[this.id]; + entity[id] = undefined; + delete entity.c[id]; + + ordered.splice(ordered.findIndex(comp => comp.id === id), 1); this.fire('remove', entity, record.data); } From e0d7e0dcac045727e6b26ff419d52dac9658f13e Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 3 Jun 2024 16:08:16 +0300 Subject: [PATCH 06/14] remove order setter --- src/framework/components/component.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/framework/components/component.js b/src/framework/components/component.js index aeb87d8004d..5c3d3f768f0 100644 --- a/src/framework/components/component.js +++ b/src/framework/components/component.js @@ -134,10 +134,6 @@ class Component extends EventHandler { * @type {number} - Component order number. * @private */ - set order(newOrder) { - this._order = newOrder; - } - get order() { return this._order; } From 9cd45e07f7c1a8dd55169694e60081c827c46d0b Mon Sep 17 00:00:00 2001 From: LeXXik Date: Sat, 15 Jun 2024 14:04:59 +0300 Subject: [PATCH 07/14] static order, sort on demand --- src/core/sort.js | 2 +- src/framework/components/component.js | 24 ++++------- .../components/rigid-body/component.js | 4 +- src/framework/components/system.js | 7 ---- src/framework/entity.js | 41 ++++++++++++------- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/core/sort.js b/src/core/sort.js index b03fcb70348..1e3197fb0db 100644 --- a/src/core/sort.js +++ b/src/core/sort.js @@ -13,7 +13,7 @@ const cmpPriority = (a, b) => a.priority - b.priority; * @returns {number} A number indicating the relative position. * @ignore */ -const cmpOrder = (a, b) => a.order - b.order; +const cmpOrder = (a, b) => a.constructor.order - b.constructor.order; /** * @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains diff --git a/src/framework/components/component.js b/src/framework/components/component.js index 5c3d3f768f0..20c3312e350 100644 --- a/src/framework/components/component.js +++ b/src/framework/components/component.js @@ -5,6 +5,15 @@ import { EventHandler } from '../../core/event-handler.js'; * events each frame, and expose properties to the PlayCanvas Editor. */ class Component extends EventHandler { + /** + * Component order. When an entity with multiple components gets enabled, this order specifies + * in which order the components get enabled. The lowest number gets enabled first. + * + * @type {number} - Component order number. + * @private + */ + static order = 0; + /** * The ComponentSystem used to create this Component. * @@ -19,9 +28,6 @@ class Component extends EventHandler { */ entity; - /** @ignore */ - _order = 0; - /** * Base constructor for a Component. * @@ -125,18 +131,6 @@ class Component extends EventHandler { get enabled() { return true; } - - /** - * Component order. When an entity with multiple components gets enabled, this order specifies - * in which order the components get enabled. The lowest number gets enabled first. By default, - * the components will be enabled in order they were added to an entity. - * - * @type {number} - Component order number. - * @private - */ - get order() { - return this._order; - } } export { Component }; diff --git a/src/framework/components/rigid-body/component.js b/src/framework/components/rigid-body/component.js index 20355869769..c655b6db971 100644 --- a/src/framework/components/rigid-body/component.js +++ b/src/framework/components/rigid-body/component.js @@ -114,6 +114,8 @@ class RigidBodyComponent extends Component { */ static EVENT_TRIGGERLEAVE = 'triggerleave'; + static order = -1; + /** @private */ _angularDamping = 0; @@ -168,8 +170,6 @@ class RigidBodyComponent extends Component { */ constructor(system, entity) { // eslint-disable-line no-useless-constructor super(system, entity); - - this._order = -1; } /** @ignore */ diff --git a/src/framework/components/system.js b/src/framework/components/system.js index 8040bad50e3..07679e438ed 100644 --- a/src/framework/components/system.js +++ b/src/framework/components/system.js @@ -4,7 +4,6 @@ import { Color } from '../../core/math/color.js'; import { Vec2 } from '../../core/math/vec2.js'; import { Vec3 } from '../../core/math/vec3.js'; import { Vec4 } from '../../core/math/vec4.js'; -import { sortOrder } from '../../core/sort.js'; /** * Component Systems contain the logic and functionality to update all Components of a particular @@ -51,9 +50,6 @@ class ComponentSystem extends EventHandler { entity[this.id] = component; entity.c[this.id] = component; - entity.oc.push(component); - sortOrder(entity.oc); - this.initializeComponentData(component, data, []); this.fire('add', entity, component); @@ -72,7 +68,6 @@ class ComponentSystem extends EventHandler { */ removeComponent(entity) { const id = this.id; - const ordered = entity.oc; const record = this.store[entity.getGuid()]; const component = entity.c[id]; @@ -83,8 +78,6 @@ class ComponentSystem extends EventHandler { entity[id] = undefined; delete entity.c[id]; - ordered.splice(ordered.findIndex(comp => comp.id === id), 1); - this.fire('remove', entity, record.data); } diff --git a/src/framework/entity.js b/src/framework/entity.js index b15f44d1272..d75fc96a31f 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -1,5 +1,6 @@ import { Debug } from '../core/debug.js'; import { guid } from '../core/guid.js'; +import { sortOrder } from '../core/sort.js'; import { GraphNode } from '../scene/graph-node.js'; @@ -11,6 +12,12 @@ import { getApplication } from './globals.js'; */ const _enableList = []; +/** + * @type {Array} + * @ignore + */ +const _sortedArray = []; + /** * The Entity is the core primitive of a PlayCanvas game. Generally speaking an object in your game * will consist of an {@link Entity}, and a set of {@link Component}s which are managed by their @@ -232,9 +239,6 @@ class Entity extends GraphNode { */ _guid = null; - /** @private */ - _oc = []; - /** * Used to differentiate between the entities of a template root instance, which have it set to * true, and the cloned instance entities (set to false). @@ -284,15 +288,6 @@ class Entity extends GraphNode { this._app = app; } - /** - * Component ordered storage. - * - * @returns {Array} - An ordered components array. - */ - get oc() { - return this._oc; - } - /** * Create a new component and add it to the entity. Use this to add functionality to the entity * like rendering a model, playing sounds and so on. @@ -515,7 +510,7 @@ class Entity extends GraphNode { _onHierarchyStateChanged(enabled) { super._onHierarchyStateChanged(enabled); - const components = this._oc; + const components = this._getSortedComponents(); for (let i = 0; i < components.length; i++) { const component = components[i]; if (component.enabled) { @@ -531,7 +526,7 @@ class Entity extends GraphNode { /** @private */ _onHierarchyStatePostChanged() { // post enable all the components - const components = this._oc; + const components = this._getSortedComponents(); for (let i = 0; i < components.length; i++) { components[i].onPostStateChange(); } @@ -609,6 +604,24 @@ class Entity extends GraphNode { return clone; } + _getSortedComponents() { + _sortedArray.length = 0; + + let needSort = false; + for (const component of Object.values(this.c)) { + if (component.constructor.order !== 0) { + needSort = true; + } + _sortedArray.push(component); + } + + if (needSort) { + sortOrder(_sortedArray); + } + + return _sortedArray; + } + /** * @param {Object} duplicatedIdsMap - A map of original entity GUIDs to cloned * entities. From 20a727ab2e037d3de95c0829e5ab1f9729ebb51f Mon Sep 17 00:00:00 2001 From: LeXXik Date: Sat, 15 Jun 2024 14:08:49 +0300 Subject: [PATCH 08/14] ts --- src/framework/entity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/framework/entity.js b/src/framework/entity.js index d75fc96a31f..9a4ff52552d 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -13,7 +13,7 @@ import { getApplication } from './globals.js'; const _enableList = []; /** - * @type {Array} + * @type {Array} * @ignore */ const _sortedArray = []; From 50d4585d7e93e5e72d764e70717f89499d8e02c5 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 13:40:37 +0300 Subject: [PATCH 09/14] cmpStaticOrder --- src/core/sort.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/sort.js b/src/core/sort.js index 1e3197fb0db..76b7788f37c 100644 --- a/src/core/sort.js +++ b/src/core/sort.js @@ -13,7 +13,7 @@ const cmpPriority = (a, b) => a.priority - b.priority; * @returns {number} A number indicating the relative position. * @ignore */ -const cmpOrder = (a, b) => a.constructor.order - b.constructor.order; +const cmpStaticOrder = (a, b) => a.constructor.order - b.constructor.order; /** * @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains @@ -29,4 +29,4 @@ export const sortPriority = arr => arr.sort(cmpPriority); * @returns {Array<{order: number}>} In place sorted array. * @ignore */ -export const sortOrder = arr => arr.sort(cmpOrder); +export const sortOrder = arr => arr.sort(cmpStaticOrder); From 143d65351dfeab56f798db8b8d1acaccb4dfe819 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 13:41:46 +0300 Subject: [PATCH 10/14] docs --- src/core/sort.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/sort.js b/src/core/sort.js index 76b7788f37c..bd69eb465d0 100644 --- a/src/core/sort.js +++ b/src/core/sort.js @@ -25,7 +25,7 @@ export const sortPriority = arr => arr.sort(cmpPriority); /** * @param {Array<{order: number}>} arr - Array to be sorted in place where each element contains - * an object with at least an `order` property. + * an object with a static `order` property. * @returns {Array<{order: number}>} In place sorted array. * @ignore */ From 13d89934f0cab13feb98c93761a231e5ee1e284a Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 13:46:26 +0300 Subject: [PATCH 11/14] remove if --- src/framework/entity.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/framework/entity.js b/src/framework/entity.js index 6559e5588db..a30d91345c4 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -607,12 +607,14 @@ class Entity extends GraphNode { _getSortedComponents() { _sortedArray.length = 0; - let needSort = false; - for (const component of Object.values(this.c)) { - if (component.constructor.order !== 0) { - needSort = true; + const components = this.c; + let needSort = 0; + for (const type in components) { + if (components.hasOwnProperty(type)) { + const component = components[type]; + needSort |= component.constructor.order !== 0; + _sortedArray.push(component); } - _sortedArray.push(component); } if (needSort) { From 1eec3427f9155b0a76e56dac66dd40aa3b3c6519 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 13:47:36 +0300 Subject: [PATCH 12/14] require at least 2 components for sorting --- src/framework/entity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/framework/entity.js b/src/framework/entity.js index a30d91345c4..2f36ec6e8eb 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -617,7 +617,7 @@ class Entity extends GraphNode { } } - if (needSort) { + if (needSort && _sortedArray.length > 1) { sortOrder(_sortedArray); } From dd81c535e8e636de5cc1d08f29860c8cbefedb32 Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 14:41:58 +0300 Subject: [PATCH 13/14] add tests --- test/framework/entity.test.mjs | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs index 51dd33c1ea3..1cd6385078e 100644 --- a/test/framework/entity.test.mjs +++ b/test/framework/entity.test.mjs @@ -32,6 +32,7 @@ import { DummyComponentSystem } from './test-component/system.mjs'; import { HTMLCanvasElement } from '@playcanvas/canvas-mock'; import { expect } from 'chai'; +import { stub } from 'sinon'; describe('Entity', function () { @@ -121,6 +122,68 @@ describe('Entity', function () { }); } + it('respects components order on disable', function () { + const entity = new Entity(); + entity.enabled = true; + + entity.addComponent('collision'); + entity.addComponent('rigidbody'); + + const colOnDisable = stub(); + const rbOnDisable = stub(); + let disableOrder = 0; + + entity.collision.onDisable = colOnDisable; + entity.rigidbody.onDisable = rbOnDisable; + + colOnDisable.onFirstCall().callsFake(() => { + disableOrder = 2; + }); + rbOnDisable.onFirstCall().callsFake(() => { + disableOrder = 1; + }); + + entity.enabled = false; + + expect(disableOrder).to.equal(2); + + entity.destroy(); + }); + + it('respects components order on enable', function () { + const entity = new Entity('Child'); + const parent = new Entity('Parent'); + + parent.addChild(entity); + parent._enabled = true; + parent._enabledInHierarchy = true; + + entity.addComponent('collision'); + entity.addComponent('rigidbody'); + + entity.enabled = false; + + const rbOnEnable = stub(); + const colOnEnable = stub(); + let enableOrder = 0; + + entity.collision.onEnable = colOnEnable; + entity.rigidbody.onEnable = rbOnEnable; + + colOnEnable.onFirstCall().callsFake(() => { + enableOrder = 2; + }); + rbOnEnable.onFirstCall().callsFake(() => { + enableOrder = 1; + }); + + entity.enabled = true; + + expect(enableOrder).to.equal(2); + + parent.destroy(); + }); + }); const createSubtree = () => { From 172f790f57bbf4b3eae7d2a1182cb0e34420146d Mon Sep 17 00:00:00 2001 From: LeXXik Date: Mon, 17 Jun 2024 14:57:55 +0300 Subject: [PATCH 14/14] sortStaticOrder --- src/core/sort.js | 2 +- src/framework/entity.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/sort.js b/src/core/sort.js index bd69eb465d0..627891ff00e 100644 --- a/src/core/sort.js +++ b/src/core/sort.js @@ -29,4 +29,4 @@ export const sortPriority = arr => arr.sort(cmpPriority); * @returns {Array<{order: number}>} In place sorted array. * @ignore */ -export const sortOrder = arr => arr.sort(cmpStaticOrder); +export const sortStaticOrder = arr => arr.sort(cmpStaticOrder); diff --git a/src/framework/entity.js b/src/framework/entity.js index 2f36ec6e8eb..059b1302c3b 100644 --- a/src/framework/entity.js +++ b/src/framework/entity.js @@ -1,6 +1,6 @@ import { Debug } from '../core/debug.js'; import { guid } from '../core/guid.js'; -import { sortOrder } from '../core/sort.js'; +import { sortStaticOrder } from '../core/sort.js'; import { GraphNode } from '../scene/graph-node.js'; @@ -618,7 +618,7 @@ class Entity extends GraphNode { } if (needSort && _sortedArray.length > 1) { - sortOrder(_sortedArray); + sortStaticOrder(_sortedArray); } return _sortedArray;