跳到主要内容

实体

实体是场景中的对象,具有名称、位置、旋转、缩放、父实体和任意数量的子实体,并通过附加的组件获得能力。本页按实体的生命周期介绍创建、命名和标签、禁用和启用、销毁及复制。

创建实体​

实体被加入场景实体树后,才成为场景的一部分;树的根为 app.root。加入之前,实体不会被绘制,脚本不会运行,物理刚体(如果有)也不参与模拟。每个实体都可以成为其他实体的父节点,因此新实体可以添加到 app.root 或场景中的任意实体下。

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

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

在脚本中,根节点为 this.app.root,this.entity 是脚本附加的实体。

名称与标签​

名称便于识别实体,会显示在编辑器层级面板中,也可用于查找实体。名称不必唯一;未指定名称的实体默认叫作 Untitled。

标签用于标记实体分组。一个实体可以有任意数量的标签,多个实体也可以共享标签。例如,为所有敌人添加 enemy 标签,就能一次查找全部敌人。参见查找实体。

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']

启用与禁用​

禁用实体时,它和层级中的所有后代都会关闭。组件停止工作:实体不再绘制,脚本的 update 不再调用,物理刚体退出模拟。但各项设置会保留,重新启用时可恢复原状。对于菜单等以后还要使用的对象,应禁用而非销毁后重新创建。

实体的 enabled 属性表示实际是否启用,因此只要任意祖先被禁用,或实体尚未加入场景,读取结果就是 false。在禁用的父实体下将 enabled 设为 true,这个设置会被记住,并在父实体重新启用后生效。

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

销毁实体​

销毁实体会将其从父节点移除,删除所有组件,并一并销毁所有后代。各实体销毁时会触发 destroy 事件,最深层的后代最先触发。已销毁实体不能再次使用,应释放持有的所有引用。

使用 removeChild() 将实体从父节点移除并不等于销毁,也不会禁用实体。脱离层级的实体不再绘制,但脚本仍会运行,灯光仍会照亮场景。要让实体停止工作,应禁用它;要彻底移除,应销毁它。

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

复制实体​

clone() 会深度复制实体,包括名称、标签、变换、启用状态、组件及其设置,以及所有后代。副本需要添加后才进入场景。复制树内部的实体引用会更新为指向副本,因此按钮引用的图像实体等对象会属于副本自身。

副本中的脚本是新实例,在副本加入场景时初始化。每个脚本从原脚本创建时的属性值开始,例如编辑器中设置的值,而非原脚本字段的当前值。

对于树木、拾取物和敌人等重复放置的对象,编辑器提供模板:将实体树保存为资源,既可放入场景,也可在运行时创建。

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

副本只能获得已在应用中注册的脚本。通过 entity.script.create(MyScript) 按类附加的脚本不会自动注册,因此必须先注册,否则副本中不会包含该脚本实例:

app.scripts.add(MyScript);

示例​

下面的引擎应用使用本页介绍的所有功能。它创建一个带脚本的立方体,脚本将立方体向上抛出,并在其落出视野后销毁;该立方体不加入场景,作为模板保留。应用每帧向 fountain 实体添加模板副本。点击画布会禁用喷泉,所有立方体消失并冻结,因为禁用实体会同时关闭后代;再次点击,它们会从原位置继续运动。

实时示例
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;
});

可以尝试:

  • 降低两处 0.05,让喷泉生成立方体的速度更快。
  • 调用 fountain.setLocalScale(2, 2, 2) 放大喷泉。立方体是它的子实体,因此绘制尺寸变为两倍,但仍沿相同路径运动,因为 translate() 在世界空间中移动实体。
  • 删除 app.scripts.add(Fall)。副本将不带脚本,因此堆积在中央,不上色,也不会被销毁。

另请参阅​