Skip to content

Commit

Permalink
Allow components ordering (#6648)
Browse files Browse the repository at this point in the history
* allow components ordering

* post changed order

* docs

* make it private

* clear on remove

* remove order setter

* static order, sort on demand

* ts

* cmpStaticOrder

* docs

* remove if

* require at least 2 components for sorting

* add tests

* sortStaticOrder
  • Loading branch information
LeXXik authored Jun 17, 2024
1 parent 5a6ff86 commit a309116
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 18 deletions.
17 changes: 17 additions & 0 deletions src/core/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,27 @@
*/
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 cmpStaticOrder = (a, b) => a.constructor.order - b.constructor.order;

/**
* @param {Array<{priority: number}>} arr - Array to be sorted in place where each element contains
* an object with at least a priority property.
* @returns {Array<{priority: number}>} In place sorted array.
* @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 a static `order` property.
* @returns {Array<{order: number}>} In place sorted array.
* @ignore
*/
export const sortStaticOrder = arr => arr.sort(cmpStaticOrder);
9 changes: 9 additions & 0 deletions src/framework/components/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
2 changes: 2 additions & 0 deletions src/framework/components/rigid-body/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class RigidBodyComponent extends Component {
*/
static EVENT_TRIGGERLEAVE = 'triggerleave';

static order = -1;

/** @private */
_angularDamping = 0;

Expand Down
7 changes: 4 additions & 3 deletions src/framework/components/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,16 @@ class ComponentSystem extends EventHandler {
* @ignore
*/
removeComponent(entity) {
const id = this.id;
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];

this.fire('remove', entity, record.data);
}
Expand Down
53 changes: 38 additions & 15 deletions src/framework/entity.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Debug } from '../core/debug.js';
import { guid } from '../core/guid.js';
import { sortStaticOrder } from '../core/sort.js';

import { GraphNode } from '../scene/graph-node.js';

Expand All @@ -11,6 +12,12 @@ import { getApplication } from './globals.js';
*/
const _enableList = [];

/**
* @type {Array<import('./components/component.js').Component>}
* @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
Expand Down Expand Up @@ -503,17 +510,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._getSortedComponents();
for (let i = 0; i < components.length; i++) {
const component = components[i];
if (component.enabled) {
if (enabled) {
component.onEnable();
} else {
component.onDisable();
}
}
}
Expand All @@ -522,10 +526,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._getSortedComponents();
for (let i = 0; i < components.length; i++) {
components[i].onPostStateChange();
}
}

Expand Down Expand Up @@ -601,6 +604,26 @@ class Entity extends GraphNode {
return clone;
}

_getSortedComponents() {
_sortedArray.length = 0;

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);
}
}

if (needSort && _sortedArray.length > 1) {
sortStaticOrder(_sortedArray);
}

return _sortedArray;
}

/**
* @param {Object<string, Entity>} duplicatedIdsMap - A map of original entity GUIDs to cloned
* entities.
Expand Down
63 changes: 63 additions & 0 deletions test/framework/entity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {

Expand Down Expand Up @@ -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 = () => {
Expand Down

0 comments on commit a309116

Please sign in to comment.