Skip to main content

Entities

An entity is an object in your scene. It has a name, a position, rotation and scale, a parent and any number of children, and it gains abilities from the components attached to it. This page follows an entity through its life: creating it, finding a name and tags for it, switching it off and on, destroying it, and making copies of it.

Creating an Entity​

An entity becomes part of the scene when it is added to the scene's tree of entities, whose root is app.root. Until then it is not drawn, its scripts do not run and its physics body, if it has one, is not simulated. Every entity can be the parent of others, so you can add a new entity to app.root or to any entity already in the scene.

const player = new pc.Entity('player');
app.root.addChild(player);

// a child of the player
const hat = new pc.Entity('hat');
player.addChild(hat);

In a script, the root is this.app.root, and this.entity is the entity that the script is attached to.

Names and Tags​

An entity's name identifies it for you. It shows in the Editor's Hierarchy, and you can look an entity up by it. Names do not have to be unique; an entity created without one is called Untitled.

Tags are labels for groups of entities. An entity can have any number of them, and many entities can share one, so you can tag every enemy enemy and find them all at once. See Finding Entities.

const enemy = new pc.Entity('goblin');
enemy.tags.add('enemy', 'melee');

enemy.name = 'goblin chief';
enemy.tags.remove('melee');
console.log(enemy.tags.list()); // ['enemy']

Enabling and Disabling​

A disabled entity is switched off, together with everything below it in the hierarchy. Its components stop: it is not drawn, its scripts' update methods are not called, and its physics body leaves the simulation. They keep their settings, though, so enabling the entity again switches it back on as it was. Disable entities that you will need again, such as a menu that comes and goes, rather than destroying and recreating them.

An entity's enabled property reports whether it is actually on, so it reads false while any of its ancestors is disabled, and for an entity that has not been added to the scene yet. Setting enabled to true under a disabled parent is remembered, and takes effect when the parent is enabled again.

door.enabled = false; // the door and all of its children switch off
door.enabled = true; // and back on

Destroying an Entity​

Destroying an entity removes it from its parent, removes all of its components and destroys all of its descendants with it. Each entity fires a destroy event as it goes, the deepest descendants first. A destroyed entity cannot be used again, so drop any references you hold to it.

Removing an entity from its parent with removeChild() is not the same thing, and does not switch it off. A detached entity stops being drawn, but its scripts keep running and its lights keep lighting the scene. To take an entity out of action, disable it. To get rid of it, destroy it.

enemy.once('destroy', () => {
console.log('enemy destroyed');
});
enemy.destroy();

Copying an Entity​

clone() makes a deep copy of an entity: its name, tags, transform and enabled state, its components with their settings, and copies of all of its descendants. The copy is not in the scene until you add it. References between entities inside the copied tree are updated to point at the copies, so a button's image entity, for example, is the copy's own image.

The scripts of a copy are new instances, which initialize when the copy joins the scene. Each one starts with the attribute values that the original script was created with, such as the ones set in the Editor, not with the current values of the original's fields.

For objects you place many times, such as trees, pickups and enemies, the Editor has templates: entity trees saved as assets, which you can place in scenes and also create at runtime.

const copy = enemy.clone();
copy.setPosition(5, 0, 0);
app.root.addChild(copy);

A copy can only get the scripts that are registered with the application. A script attached by class with entity.script.create(MyScript) is not registered automatically, so register it first, or the copy has no instance of it:

app.scripts.add(MyScript);

Example​

This Engine app uses everything on this page. It builds one cube with a script that throws the cube up and destroys it once it falls out of view, and keeps that cube out of the scene as a template. Every frame, it adds copies of the template to a fountain entity. Click the canvas to disable the fountain: every cube in it disappears and freezes, because disabling an entity switches off its descendants. Click again, and they carry on from where they were.

Live Example
import * as pc from 'playcanvas';

// create the application with the component systems it uses
const canvas = document.getElementById('application');
const device = await pc.createGraphicsDevice(canvas, {
deviceTypes: [pc.DEVICETYPE_WEBGPU]
});
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const options = new pc.AppOptions();
options.graphicsDevice = device;
options.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.ScriptComponentSystem
];

const app = new pc.AppBase(canvas);
app.init(options);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());
app.start();

app.scene.ambientLight = new pc.Color(0.3, 0.3, 0.35);

const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.11, 0.12, 0.17)
});
camera.setPosition(0, 3, 14);
camera.lookAt(0, 3, 0);
app.root.addChild(camera);

const light = new pc.Entity('light');
light.addComponent('light');
light.setEulerAngles(45, 30, 0);
app.root.addChild(light);

// three materials for the cubes to pick from
const materials = [
new pc.Color(1, 0.55, 0.2),
new pc.Color(0.4, 0.2, 1),
new pc.Color(0.2, 0.8, 0.6)
].map((color) => {
const material = new pc.StandardMaterial();
material.diffuse = color;
material.update();
return material;
});

// a script that throws its entity up, and destroys it once it falls out of view
class Fall extends pc.Script {
static scriptName = 'fall';

velocity = new pc.Vec3();

initialize() {
this.velocity.set(pc.math.random(-2, 2), pc.math.random(8, 11), pc.math.random(-2, 2));
this.entity.render.material = materials[Math.floor(Math.random() * materials.length)];
}

update(dt) {
this.velocity.y -= 9.8 * dt;
this.entity.translate(this.velocity.x * dt, this.velocity.y * dt, this.velocity.z * dt);
this.entity.rotateLocal(120 * dt, 80 * dt, 0);
if (this.entity.getPosition().y < -4) {
this.entity.destroy();
}
}
}

// clone() can only copy scripts that are registered with the application
app.scripts.add(Fall);

// the template is never added to the scene, so it is never drawn and its script never runs
const template = new pc.Entity('cube');
template.addComponent('render', { type: 'box' });
template.addComponent('script');
template.script.create(Fall);
template.setLocalScale(0.3, 0.3, 0.3);

// the fountain holds every cube, so disabling it disables them all
const fountain = new pc.Entity('fountain');
app.root.addChild(fountain);

const label = document.createElement('div');
label.style.cssText = 'position: absolute; left: 12px; bottom: 10px; color: #ccc; font: 14px sans-serif;';
document.body.appendChild(label);

// every frame, while the fountain is enabled, add copies of the template to it
let timer = 0;
app.on('update', (dt) => {
if (fountain.enabled) {
timer += dt;
while (timer > 0.05) {
timer -= 0.05;
fountain.addChild(template.clone());
}
}
label.textContent = `Cubes: ${fountain.children.length}. Click to ${fountain.enabled ? 'disable' : 'enable'} the fountain.`;
});

// disable and enable the fountain, and with it every cube
canvas.addEventListener('click', () => {
fountain.enabled = !fountain.enabled;
});

Some things to try:

  • Make the fountain spray faster by lowering the 0.05 in both places it appears.
  • Scale the fountain up with fountain.setLocalScale(2, 2, 2). Its cubes are its children, so they are drawn twice as large. They still fly the same paths, though, because translate() moves an entity in world space.
  • Delete the app.scripts.add(Fall) line. The copies then have no script, so they pile up in the middle, uncolored, and are never destroyed.

See Also​