メインコンテンツまでスキップ

エンティティ

エンティティは、シーン内のオブジェクトです。名前、位置、回転、スケール、親と任意の数の子を持ち、アタッチされたコンポーネントから能力を得ます。このページでは、エンティティの一生をたどります。作成し、名前とタグを付け、オフとオンを切り替え、破棄し、複製するまでです。

エンティティの作成​

エンティティは、app.rootをルートとするシーンのエンティティのツリーに追加されたときに、シーンの一部になります。それまでは描画されず、スクリプトは実行されず、物理ボディを持っていてもシミュレートされません。どのエンティティもほかのエンティティの親になれるので、新しいエンティティはapp.rootにも、シーンにすでにある任意のエンティティにも追加できます。

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

// playerの子
const hat = new pc.Entity('hat');
player.addChild(hat);

スクリプトの中では、ルートはthis.app.rootで、this.entityはスクリプトがアタッチされているエンティティです。

名前とタグ​

エンティティの名前は、開発者がエンティティを見分けるためのものです。エディターのヒエラルキーに表示され、名前でエンティティを検索することもできます。名前は一意である必要はありません。名前を付けずに作成したエンティティはUntitledと呼ばれます。

タグは、エンティティのグループに付けるラベルです。1つのエンティティに任意の数のタグを付けられ、多くのエンティティで1つのタグを共有できるので、たとえばすべての敵に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; // doorとそのすべての子がオフになります
door.enabled = true; // そしてオンに戻ります

エンティティの破棄​

エンティティを破棄すると、エンティティは親から取り除かれ、そのコンポーネントはすべて削除され、子孫もすべて一緒に破棄されます。その過程で、各エンティティが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);

例​

このEngineアプリは、このページの内容をすべて使います。キューブを放り上げ、見えないところまで落ちたら破棄するスクリプトを持つキューブを1つ作り、そのキューブをテンプレートとしてシーンの外に置いておきます。そして毎フレーム、テンプレートのコピーをfountainエンティティに追加します。キャンバスをクリックしてfountainを無効にすると、その中のすべてのキューブが消えて止まります。エンティティを無効にすると、その子孫もオフになるからです。もう一度クリックすると、キューブは止まったところから動き出します。

ライブサンプル
import * as pc from 'playcanvas';

// 使用するコンポーネントシステムを指定してアプリケーションを作成します
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);

// キューブが選ぶ3つのマテリアル
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;
});

// エンティティを放り上げ、見えないところまで落ちたら破棄するスクリプト
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()がコピーできるのは、アプリケーションに登録されたスクリプトだけです
app.scripts.add(Fall);

// テンプレートはシーンに追加しないので、描画されることもスクリプトが実行されることもありません
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);

// fountainがすべてのキューブを持つので、fountainを無効にするとすべてが無効になります
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);

// fountainが有効な間、毎フレームfountainにテンプレートのコピーを追加します
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.`;
});

// fountainを、それと一緒にすべてのキューブを、無効化・有効化します
canvas.addEventListener('click', () => {
fountain.enabled = !fountain.enabled;
});

次のような変更を試してみてください。

  • 2か所にある0.05を小さくすると、fountainがより速く噴き出します。
  • fountain.setLocalScale(2, 2, 2)でfountainを大きくします。キューブはfountainの子なので、2倍の大きさで描画されます。ただし、translate()はワールド空間でエンティティを動かすので、飛ぶ軌道は変わりません。
  • app.scripts.add(Fall)の行を削除します。するとコピーはスクリプトを持たないので、色の付かないまま中央に積み重なり、破棄されることもありません。

関連情報​