playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
630 lines (629 loc) • 20.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../core/debug.js";
import { guid } from "../core/guid.js";
import { GraphNode } from "../scene/graph-node.js";
import { getApplication } from "./globals.js";
const cmpStaticOrder = (a, b) => a.constructor.order - b.constructor.order;
const sortStaticOrder = (arr) => arr.sort(cmpStaticOrder);
const _enableList = [];
const tmpPool = [];
const getTempArray = () => {
return tmpPool.pop() ?? [];
};
const releaseTempArray = (a) => {
a.length = 0;
tmpPool.push(a);
};
const _Entity = class _Entity extends GraphNode {
/**
* Create a new Entity.
*
* @param {string} [name] - The non-unique name of the entity, default is "Untitled".
* @param {AppBase} [app] - The application the entity belongs to, default is the current
* application.
* @example
* const entity = new pc.Entity();
*
* // Add a Component to the Entity
* entity.addComponent('camera', {
* fov: 45,
* nearClip: 1,
* farClip: 10000
* });
*
* // Add the Entity into the scene graph
* app.root.addChild(entity);
*
* // Move the entity
* entity.translate(10, 0, 0);
*
* // Or translate it by setting its position directly
* const p = entity.getPosition();
* entity.setPosition(p.x + 10, p.y, p.z);
*
* // Change the entity's rotation in local space
* const e = entity.getLocalEulerAngles();
* entity.setLocalEulerAngles(e.x, e.y + 90, e.z);
*
* // Or use rotateLocal
* entity.rotateLocal(0, 90, 0);
*/
constructor(name, app = getApplication()) {
super(name);
/**
* Gets the {@link AnimComponent} attached to this entity.
*
* @type {AnimComponent|undefined}
* @readonly
*/
__publicField(this, "anim");
/**
* Gets the {@link AnimationComponent} attached to this entity.
*
* @type {AnimationComponent|undefined}
* @readonly
*/
__publicField(this, "animation");
/**
* Gets the {@link AudioListenerComponent} attached to this entity.
*
* @type {AudioListenerComponent|undefined}
* @readonly
*/
__publicField(this, "audiolistener");
/**
* Gets the {@link ButtonComponent} attached to this entity.
*
* @type {ButtonComponent|undefined}
* @readonly
*/
__publicField(this, "button");
/**
* Gets the {@link CameraComponent} attached to this entity.
*
* @type {CameraComponent|undefined}
* @readonly
*/
__publicField(this, "camera");
/**
* Gets the {@link CollisionComponent} attached to this entity.
*
* @type {CollisionComponent|undefined}
* @readonly
*/
__publicField(this, "collision");
/**
* Gets the {@link ElementComponent} attached to this entity.
*
* @type {ElementComponent|undefined}
* @readonly
*/
__publicField(this, "element");
/**
* Gets the {@link GSplatComponent} attached to this entity.
*
* @type {GSplatComponent|undefined}
* @readonly
*/
__publicField(this, "gsplat");
/**
* Gets the {@link LayoutChildComponent} attached to this entity.
*
* @type {LayoutChildComponent|undefined}
* @readonly
*/
__publicField(this, "layoutchild");
/**
* Gets the {@link LayoutGroupComponent} attached to this entity.
*
* @type {LayoutGroupComponent|undefined}
* @readonly
*/
__publicField(this, "layoutgroup");
/**
* Gets the {@link LightComponent} attached to this entity.
*
* @type {LightComponent|undefined}
* @readonly
*/
__publicField(this, "light");
/**
* Gets the {@link ModelComponent} attached to this entity.
*
* @type {ModelComponent|undefined}
* @readonly
*/
__publicField(this, "model");
/**
* Gets the {@link ParticleSystemComponent} attached to this entity.
*
* @type {ParticleSystemComponent|undefined}
* @readonly
*/
__publicField(this, "particlesystem");
/**
* Gets the {@link RenderComponent} attached to this entity.
*
* @type {RenderComponent|undefined}
* @readonly
*/
__publicField(this, "render");
/**
* Gets the {@link RigidBodyComponent} attached to this entity.
*
* @type {RigidBodyComponent|undefined}
* @readonly
*/
__publicField(this, "rigidbody");
/**
* Gets the {@link ScreenComponent} attached to this entity.
*
* @type {ScreenComponent|undefined}
* @readonly
*/
__publicField(this, "screen");
/**
* Gets the {@link ScriptComponent} attached to this entity.
*
* @type {ScriptComponent|undefined}
* @readonly
*/
__publicField(this, "script");
/**
* Gets the {@link ScrollbarComponent} attached to this entity.
*
* @type {ScrollbarComponent|undefined}
* @readonly
*/
__publicField(this, "scrollbar");
/**
* Gets the {@link ScrollViewComponent} attached to this entity.
*
* @type {ScrollViewComponent|undefined}
* @readonly
*/
__publicField(this, "scrollview");
/**
* Gets the {@link SoundComponent} attached to this entity.
*
* @type {SoundComponent|undefined}
* @readonly
*/
__publicField(this, "sound");
/**
* Gets the {@link SpriteComponent} attached to this entity.
*
* @type {SpriteComponent|undefined}
* @readonly
*/
__publicField(this, "sprite");
/**
* Component storage.
*
* @type {Object<string, Component>}
* @ignore
*/
__publicField(this, "c", {});
/**
* @type {AppBase}
* @private
*/
__publicField(this, "_app");
/**
* Used by component systems to speed up destruction.
*
* @ignore
*/
__publicField(this, "_destroying", false);
/**
* @type {string|null}
* @private
*/
__publicField(this, "_guid", null);
/**
* 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).
*
* @ignore
*/
__publicField(this, "_template", false);
Debug.assert(app, "Could not find current application");
this._app = app;
}
/**
* 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.
*
* @param {string} type - The name of the component to add. Valid strings are:
*
* - "anim" - see {@link AnimComponent}
* - "animation" - see {@link AnimationComponent}
* - "audiolistener" - see {@link AudioListenerComponent}
* - "button" - see {@link ButtonComponent}
* - "camera" - see {@link CameraComponent}
* - "collision" - see {@link CollisionComponent}
* - "element" - see {@link ElementComponent}
* - "gsplat" - see {@link GSplatComponent}
* - "layoutchild" - see {@link LayoutChildComponent}
* - "layoutgroup" - see {@link LayoutGroupComponent}
* - "light" - see {@link LightComponent}
* - "model" - see {@link ModelComponent}
* - "particlesystem" - see {@link ParticleSystemComponent}
* - "render" - see {@link RenderComponent}
* - "rigidbody" - see {@link RigidBodyComponent}
* - "screen" - see {@link ScreenComponent}
* - "script" - see {@link ScriptComponent}
* - "scrollbar" - see {@link ScrollbarComponent}
* - "scrollview" - see {@link ScrollViewComponent}
* - "sound" - see {@link SoundComponent}
* - "sprite" - see {@link SpriteComponent}
*
* @param {object} [data] - The initialization data for the specific component type. Refer to
* each specific component's API reference page for details on valid values for this parameter.
* @returns {Component|null} The new Component that was attached to the entity or null if there
* was an error.
* @example
* const entity = new pc.Entity();
*
* // Add a light component with default properties
* entity.addComponent("light");
*
* // Add a camera component with some specified properties
* entity.addComponent("camera", {
* fov: 45,
* clearColor: new pc.Color(1, 0, 0)
* });
*/
addComponent(type, data) {
const system = this._app.systems[type];
if (!system) {
Debug.error(`addComponent: System '${type}' doesn't exist`);
return null;
}
if (this.c[type]) {
Debug.warn(`addComponent: Entity already has '${type}' component`);
return null;
}
return system.addComponent(this, data);
}
/**
* Remove a component from the Entity.
*
* @param {string} type - The name of the Component type.
* @example
* const entity = new pc.Entity();
* entity.addComponent("light"); // add new light component
*
* entity.removeComponent("light"); // remove light component
*/
removeComponent(type) {
const system = this._app.systems[type];
if (!system) {
Debug.error(`removeComponent: System '${type}' doesn't exist`);
return;
}
if (!this.c[type]) {
Debug.warn(`removeComponent: Entity doesn't have '${type}' component`);
return;
}
system.removeComponent(this);
}
/**
* Search the entity and all of its descendants for the first component of specified type.
*
* @param {string} type - The name of the component type to retrieve.
* @returns {Component} A component of specified type, if the entity or any of its descendants
* has one. Returns undefined otherwise.
* @example
* // Get the first found light component in the hierarchy tree that starts with this entity
* const light = entity.findComponent("light");
*/
findComponent(type) {
const entity = this.findOne((entity2) => entity2.c?.[type]);
return entity && entity.c[type];
}
/**
* Search the entity and all of its descendants for all components of specified type.
*
* @param {string} type - The name of the component type to retrieve.
* @returns {Component[]} All components of specified type in the entity or any of its
* descendants. Returns empty array if none found.
* @example
* // Get all light components in the hierarchy tree that starts with this entity
* const lights = entity.findComponents("light");
*/
findComponents(type) {
return this.find((entity) => entity.c?.[type]).map((entity) => entity.c[type]);
}
/**
* Search the entity and all of its descendants for the first script instance of specified type.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {ScriptType|undefined} A script instance of specified type, if the entity or any of
* its descendants has one. Returns undefined otherwise.
* @example
* // Get the first found "playerController" instance in the hierarchy tree that starts with this entity
* const controller = entity.findScript("playerController");
*/
findScript(nameOrType) {
const entity = this.findOne((node) => node.c?.script?.has(nameOrType));
return entity?.c.script.get(nameOrType);
}
/**
* Search the entity and all of its descendants for all script instances of specified type.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {ScriptType[]} All script instances of specified type in the entity or any of its
* descendants. Returns empty array if none found.
* @example
* // Get all "playerController" instances in the hierarchy tree that starts with this entity
* const controllers = entity.findScripts("playerController");
*/
findScripts(nameOrType) {
const entities = this.find((node) => node.c?.script?.has(nameOrType));
return entities.map((entity) => entity.c.script.get(nameOrType));
}
/**
* Sets the GUID for this Entity. Note that it is unlikely that you should need to change the
* GUID value of an Entity at run-time. Doing so will corrupt the graph this Entity is in.
*
* @type {string}
* @ignore
*/
set guid(value) {
const index = this._app._entityIndex;
if (this._guid) {
delete index[this._guid];
}
this._guid = value;
index[this._guid] = this;
}
/**
* Gets the GUID for this Entity.
*
* @type {string}
*/
get guid() {
if (!this._guid) {
this.guid = guid.create();
}
return this._guid;
}
/**
* Get the GUID value for this Entity.
*
* @returns {string} The GUID of the Entity.
* @ignore
* @deprecated Use {@link Entity#guid} instead.
*/
getGuid() {
Debug.deprecated("Entity#getGuid is deprecated. Use Entity#guid instead.");
return this.guid;
}
/**
* Set the GUID value for this Entity. Note that it is unlikely that you should need to change
* the GUID value of an Entity at run-time. Doing so will corrupt the graph this Entity is in.
*
* @param {string} guid - The GUID to assign to the Entity.
* @ignore
* @deprecated Use {@link Entity#guid} instead.
*/
setGuid(guid2) {
Debug.deprecated("Entity#setGuid is deprecated. Use Entity#guid instead.");
this.guid = guid2;
}
/**
* @param {GraphNode} node - The node to update.
* @param {boolean} enabled - Enable or disable the node.
* @protected
*/
_notifyHierarchyStateChanged(node, enabled) {
let enableFirst = false;
if (node === this && _enableList.length === 0) {
enableFirst = true;
}
node._beingEnabled = true;
node._onHierarchyStateChanged(enabled);
if (node._onHierarchyStatePostChanged) {
_enableList.push(node);
}
const c = node._children;
for (let i = 0, len = c.length; i < len; i++) {
if (c[i]._enabled) {
this._notifyHierarchyStateChanged(c[i], enabled);
}
}
node._beingEnabled = false;
if (enableFirst) {
for (let i = 0; i < _enableList.length; i++) {
_enableList[i]._onHierarchyStatePostChanged();
}
_enableList.length = 0;
}
}
/**
* @param {boolean} enabled - Enable or disable the node.
* @protected
*/
_onHierarchyStateChanged(enabled) {
super._onHierarchyStateChanged(enabled);
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();
}
}
}
releaseTempArray(components);
}
/** @private */
_onHierarchyStatePostChanged() {
const components = this._getSortedComponents();
for (let i = 0; i < components.length; i++) {
components[i].onPostStateChange();
}
releaseTempArray(components);
}
/**
* Find a descendant of this entity with the GUID.
*
* @param {string} guid - The GUID to search for.
* @returns {Entity|null} The entity with the matching GUID or null if no entity is found.
*/
findByGuid(guid2) {
if (this._guid === guid2) return this;
const e = this._app._entityIndex[guid2];
if (e && (e === this || e.isDescendantOf(this))) {
return e;
}
return null;
}
/**
* Destroy the entity and all of its descendants. First, all of the entity's components are
* disabled and then removed. Then, the entity is removed from the hierarchy. This is then
* repeated recursively for all descendants of the entity.
*
* The last thing the entity does is fire the `destroy` event.
*
* @example
* const firstChild = this.entity.children[0];
* firstChild.destroy(); // destroy child and all of its descendants
*/
destroy() {
this._destroying = true;
for (const name in this.c) {
this.c[name].enabled = false;
}
for (const name in this.c) {
this.c[name].system.removeComponent(this);
}
super.destroy();
if (this._guid) {
delete this._app._entityIndex[this._guid];
}
this._destroying = false;
}
/**
* Create a deep copy of the Entity. Duplicate the full Entity hierarchy, with all Components
* and all descendants. Note, this Entity is not in the hierarchy and must be added manually.
*
* @returns {this} A new Entity which is a deep copy of the original.
* @example
* const e = this.entity.clone();
*
* // Add clone as a sibling to the original
* this.entity.parent.addChild(e);
*/
clone() {
const duplicatedIdsMap = {};
const clone = this._cloneRecursively(duplicatedIdsMap);
duplicatedIdsMap[this.guid] = clone;
resolveDuplicatedEntityReferenceProperties(this, this, clone, duplicatedIdsMap);
return clone;
}
_getSortedComponents() {
const components = this.c;
const sortedArray = getTempArray();
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.
* @returns {this} A new Entity which is a deep copy of the original.
* @private
*/
_cloneRecursively(duplicatedIdsMap) {
const clone = new this.constructor(void 0, this._app);
super._cloneInternal(clone);
for (const type in this.c) {
const component = this.c[type];
component.system.cloneComponent(this, clone);
}
for (let i = 0; i < this._children.length; i++) {
const oldChild = this._children[i];
if (oldChild instanceof _Entity) {
const newChild = oldChild._cloneRecursively(duplicatedIdsMap);
clone.addChild(newChild);
duplicatedIdsMap[oldChild.guid] = newChild;
}
}
return clone;
}
};
/**
* Fired after the entity is destroyed.
*
* @event
* @example
* entity.on('destroy', (e) => {
* console.log(`Entity ${e.name} has been destroyed`);
* });
*/
__publicField(_Entity, "EVENT_DESTROY", "destroy");
let Entity = _Entity;
function resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, oldEntity, newEntity, duplicatedIdsMap) {
if (oldEntity instanceof Entity) {
const components = oldEntity.c;
for (const componentName in components) {
const component = components[componentName];
const entityProperties = component.system.getPropertiesOfType("entity");
for (let i = 0, len = entityProperties.length; i < len; i++) {
const propertyDescriptor = entityProperties[i];
const propertyName = propertyDescriptor.name;
const oldEntityReferenceId = component[propertyName];
const entityIsWithinOldSubtree = !!oldSubtreeRoot.findByGuid(oldEntityReferenceId);
if (entityIsWithinOldSubtree) {
const newEntityReferenceId = duplicatedIdsMap[oldEntityReferenceId].guid;
if (newEntityReferenceId) {
newEntity.c[componentName][propertyName] = newEntityReferenceId;
} else {
Debug.warn("Could not find corresponding entity id when resolving duplicated entity references");
}
}
}
}
if (components.script) {
newEntity.script.resolveDuplicatedEntityReferenceProperties(components.script, duplicatedIdsMap);
}
if (components.render) {
newEntity.render.resolveDuplicatedEntityReferenceProperties(components.render, duplicatedIdsMap);
}
if (components.button) {
newEntity.button.resolveDuplicatedEntityReferenceProperties(components.button, duplicatedIdsMap);
}
if (components.scrollview) {
newEntity.scrollview.resolveDuplicatedEntityReferenceProperties(components.scrollview, duplicatedIdsMap);
}
if (components.scrollbar) {
newEntity.scrollbar.resolveDuplicatedEntityReferenceProperties(components.scrollbar, duplicatedIdsMap);
}
if (components.anim) {
newEntity.anim.resolveDuplicatedEntityReferenceProperties(components.anim, duplicatedIdsMap);
}
const _old = oldEntity.children.filter((e) => e instanceof Entity);
const _new = newEntity.children.filter((e) => e instanceof Entity);
for (let i = 0, len = _old.length; i < len; i++) {
resolveDuplicatedEntityReferenceProperties(oldSubtreeRoot, _old[i], _new[i], duplicatedIdsMap);
}
}
}
export {
Entity
};