エンティティコンポーネントシステムパターンでは、コンポーネントは再利用可能でモジュール化されたデータの塊で、外観、動作、機能性を追加するためにエンティティに差し込まれます。

A-Frameでは、コンポーネントは、シーン内の3Dオブジェクトであるエンティティを変更します。コンポーネントを組み合わせて、複雑なオブジェクトを構築することができます。コンポーネントは、three.js (opens new window)JavaScriptのコードをモジュールにカプセル化し、HTMLから宣言的に使用できるようにするものです。

抽象的な例として、スマートフォンをエンティティとして定義する場合、コンポーネントを使用して外観(色、形)を与えたり、動作(呼び出されたら振動する、バッテリーが少なくなったらシャットダウンする)を定義したり、機能(カメラ、画面)を追加することができます。

コンポーネントは、CSSとほぼ同じようなものです。CSSのルールが要素の外観を変更するように、コンポーネントのプロパティはエンティティの外観、動作、機能を変更します。

# コンポーネントHTMLの形状

コンポーネントは、1つまたは複数のコンポーネントプロパティという形でデータバケットを保持します。 コンポーネントはこのデータを使ってエンティティを変更します。例えば、「エンジン」コンポーネントの場合、「馬力」や「シリンダー数」などのプロパティを定義することができます。

HTML属性はコンポーネント名を表し、その属性の値はコンポーネントデータを表します。

# シングルプロパティコンポーネント

コンポーネントが単一プロパティのコンポーネントである場合、つまりそのデータが単一の値で構成されている場合、HTMLではコンポーネントの値は通常のHTML属性のように見えます。

<!-- `position` is the name of the position component. -->
<!-- `1 2 3` is the data of the position component. -->
<a-entity position="1 2 3"></a-entity>

# マルティプロパティコンポーネント

コンポーネントがマルチプロパティコンポーネント、つまりデータが複数のプロパティと値で構成されている場合、HTMLではコンポーネント値はインラインCSSスタイルと似ています。

<!-- `light` is the name of the light component. -->
<!-- The `type` property of the light is set to `point`. -->
<!-- The `color` property of the light is set to `crimson`. -->
<a-entity light="type: point; color: crimson"></a-entity>

# コンポーネントの登録

# AFRAME.registerComponent (name, definition)

A-Frameコンポーネントを登録します。<a-scene> 内のどこかでコンポーネントを使用する前に、コンポーネントを登録する必要があります。HTMLファイルの仕組みと同じように、コンポーネントは<a-scene>の前に順番に来るはずです。

  • {string} name - コンポーネントの名前。HTMLの属性名で表されるコンポーネントの公開APIです。
  • {Object} definition - コンポーネントの定義。スキーマとライフサイクルハンドラメソッドが含まれます。
// Registering component in foo-component.js
AFRAME.registerComponent('foo', {
  schema: {},
  init: function () {},
  update: function () {},
  tick: function () {},
  remove: function () {},
  pause: function () {},
  play: function () {}
});
<!-- Usage of `foo` component. -->
<html>
  <head>
    <script2 src="aframe.min.js"></script>
    <script2 src="foo-component.js"></script>
  </head>
  <body>
    <a-scene>
      <a-entity foo></a-entity>
    </a-scene>
  </body>
</html>

# スキーマ

スキーマは、コンポーネントの1つまたは複数のプロパティを定義し、記述するオブジェクトです。スキーマのキーはプロパティの名前であり、スキーマの値はプロパティの型と値を定義します(マルチプロパティコンポーネントの場合)。

AFRAME.registerComponent('bar', {
  schema: {
    color: {default: '#FFF'},
    size: {type: 'int', default: 5}
  }
}
<a-scene>
  <a-entity bar="color: red; size: 20"></a-entity>
</a-scene>

propsimage Image by Ruben Mueller from vrjump.de

# プロパティタイプ

プロパティタイプは主に、スキーマが各プロパティに対してDOMから入力されるデータをどのようにパースするかを定義します。 パースされたデータは、コンポーネントのプロトタイプの data プロパティを介して利用することができます。 以下は、A-Frameの組み込みのプロパティタイプです。

Property Type 解説 Default Value
array カンマで区切られた値を配列にパースします。 (例: "1, 2, 3" to ['1', '2', '3']). []
asset 一般的なアセットを指すURL。url(<url>)形式の文字列でURLをパースすることができます。 値が要素IDセレクタ(例:#texture)の場合、このプロパティタイプはgetElementByIdgetAttribute('src')を呼び出してURLを返します。Asset プロパティタイプは、XHR を処理したり、MediaElement を直接返す (例: <img> 要素) ように変更することもできますし、そうしないこともできます。 ''
audio アセットプロパティタイプと同じ構文解析です。A-Frameインスペクタで、オーディオアセットを表示するために使用される可能性があります。 ''
boolean 文字列をブール型に解析します (すなわち、"false" を false に、それ以外をtrueにします). false
color 現在、パース処理は行っていません。主にA-Frame Inspectorで、カラーピッカーを表示するために使用されます。また、カラーアニメーションを動作させるためには、カラータイプを使用する必要があります。 #FFF
int parseIntを呼びます (例: "124.5" to 124). 0
map アセットプロパティタイプと同じ構文解析です。A-Frameインスペクタで、テクスチャアセットを表示するために使用される可能性があります。
    | ''                       |

| model | アセットプロパティタイプと同じパース処理を行います。A-Frameインスペクタで、モデルアセットを表示するために使用される可能性があります。 | '' | | number | parseFloatを呼び出します (例: "124.5" to 124.5). | 0 | | selector | querySelector を呼び出します (例:, "#box" to <a-entity id="box">). | null | | selectorAll | querySelectorAll を呼び出し、 NodeListArray にコンバートします (例:, ".boxes" を [<a-entity class="boxes", ...]), | null | | string | パースを行いません | '' | | vec2 | 2つの数値を {x, y} の形式にします (例:, 1 -2 to {x: 1, y: -2}. | {x: 0, y: 0} | | vec3 | 3つの数値を {x, y, z} の形式にします (例:, 1 -2 3 to {x: 1, y: -2, z: 3}. | {x: 0, y: 0, z: 0} | | vec4 | 4つの数値を {x, y, z, w} の形式にします (例:, 1 -2 3 -4.5 to {x: 1, y: -2, z: 3, w: -4.5}. | {x: 0, y: 0, z: 0, w: 0} |

# プロパティタイプの推論

スキーマは、デフォルト値のみを与えられたプロパティタイプを推論しようとします。

schema: {default: 10}  // type: "number"
schema: {default: "foo"}  // type: "string"
schema: {default: [1, 2, 3]}  // type: "array"

スキーマは、プロパティの種類を考慮して、提供されない場合はデフォルト値を設定する。

schema: {type: 'number'}  // default: 0
schema: {type: 'string'}  // default: ''
schema: {type: 'vec3'}  // default: {x: 0, y: 0, z: 0}

# カスタムプロパティタイプ

また、 parsestringify関数を type の代わりに提供することで、独自のプロパティタイプを定義することができます。

schema: {
  // Parse slash-delimited string to an array (e.g., `foo="myProperty: a/b"` to `['a', 'b']`),
  // stringify array to string (e.g., `['a', 'b']` to `foo="myProperty: a/b"`)
  myProperty: {
    default: [],
    parse: function (value) {
      return value.split('/');
    },
    stringify: function (value) {
      return value.join('/');
    }
  }
}

setAttributeメソッドでコンポーネントのプロパティが更新されるとparseが呼ばれ、flushToDomメソッドでDOMが更新されるとstringifyが呼ばれます。

# シングル・プロパティ・スキーマ

コンポーネントは、シングルプロパティコンポーネント(1つのアノニマス値で構成される)またはマルチプロパティコンポーネント(複数の名前付き値で構成される)のいずれかでいることができます。A-Frameは、スキーマの構造に基づいて、コンポーネントがシングルプロパティかマルチプロパティかを推論します。

シングルプロパティコンポーネントのスキーマには、タイプキーやデフォルトキーが含まれ、スキーマの値は、オブジェクトではなく、プレーンな値です。

AFRAME.registerComponent('foo', {
  schema: {type: 'int', default: 5}
});
<a-scene>
  <a-entity foo="20"></a-entity>
</a-scene>

# ライフサイクル定義ハンドラーメソッド

スキーマが解剖学ならば、ライフサイクル・メソッドは生理学です。 スキーマがデータの形を定義し、ライフサイクルハンドラメソッドがデータを使ってエンティティを変更します。ハンドラは通常 Entity APIと協調します。

methodsimage Lifecycle method handlers. Image by Ruben Mueller from vrjump.de

# Overview of Methods

Method Description
init コンポーネントが初期化されたときに一度だけ呼び出されます。初期状態の設定と変数のインスタンス化に使用されます。
update コンポーネントが初期化されたときと、コンポーネントのプロパティのいずれかが更新されるたびに呼び出されます (例: *setAttribute*を使用)。エンティティを変更するために使用されます。
remove コンポーネントがエンティティから削除されたとき(例:removeAttribute経由)、またはエンティティがシーンから切り離されたときに呼び出される。エンティティに対する以前のすべての変更を取り消すために使用されます。
tick レンダリングループまたはシーンのティックごとに呼び出されます。継続的な変更やチェックのために使用されます。
tock シーンがレンダリングされた後、各レンダリングループまたはシーンのtickで呼び出される。シーンが描画された後に起こる必要がある、ポスト処理エフェクトやその他のロジックに使用されます。
play シーンやエンティティが再生され、背景や動的な動作が追加されるたびに呼び出されます。また、コンポーネントが初期化されたときにも一度だけ呼び出されます。ビヘイビアを開始または再開するために使用されます。
pause シーンまたはエンティティが一時停止し、背景または動的な動作を削除するたびに呼び出されます。また、コンポーネントがエンティティから削除されたとき、またはエンティティがシーンから切り離されたときにも呼び出されます。ビヘイビアを一時停止するために使用されます。
updateSchema コンポーネントのプロパティのいずれかが更新されるたびに呼び出されます。スキーマを動的に変更するために使用することができます。

# コンポーネント プロトタイプ プロパティ

メソッド内では、thisを介してコンポーネントのプロトタイプにアクセスすることができます。

Property Description
this.data スキーマのデフォルト値、Mix-in、およびエンティティの属性から計算してパースされたコンポーネントプロパティ。

重要: data 属性を直接変更しないでください。A-Frameの内部で更新されます。 コンポーネントを更新するにはsetAttributeを使ってください。
this.el HTML要素としてのentityを参照します。
this.el.sceneEl HTML要素としてのsceneを参照する。
this.id コンポーネントが複数のインスタンスを持つことができる場合、コンポーネントの個々のインスタンスのIDを指します (例: sound__foo から foo)

# .init ()

.init ()は、コンポーネントのライフサイクルの最初に一度だけ呼ばれます。エンティティは、コンポーネントのinitハンドラを呼び出すことができます。

  • コンポーネントがHTMLファイル内のエンティティに静的に設定され、ページがロードされたとき。
  • コンポーネントが、setAttributeを介してアタッチされたエンティティに設定されたとき。
  • コンポーネントがアタッチされていないエンティティにセットされ、そのエンティティがappendChildによってシーンにアタッチされたとき。

initハンドラは、次のような用途によく使われます。

  • 初期状態および変数のセットアップ
  • メソッドのバインド
  • イベントリスナーの取り付け

例えば、カーソルコンポーネントのinitは、ステート変数の設定、メソッドのバインド、イベントリスナーの追加を行います。

AFRAME.registerComponent('cursor', {
  // ...
  init: function () {
    // Set up initial state and variables.
    this.intersection = null;
    // Bind methods.
    this.onIntersection = AFRAME.utils.bind(this.onIntersection, this);
    // Attach event listener.
    this.el.addEventListener('raycaster-intersection', this.onIntersection);
  }
  // ...

# .update (oldData)

.update (oldData)は、コンポーネントのライフサイクルの最初を含め、コンポーネントのプロパティが変更されるたびに呼び出されます。エンティティは、コンポーネントのupdateハンドラを呼び出すことができます。

  • init()が呼ばれた後、コンポーネントのライフサイクルの開始時。
  • コンポーネントのプロパティが.setAttributeで更新されたとき。

update ハンドラは、しばしば次のような目的で使用されます。

  • エンティティに変更を加える際のほとんどの作業は、this.dataを使用して行われる。
  • 1つまたは複数のコンポーネントプロパティが変更されるたびに、エンティティを変更する。

現在のデータセット (this.data) と更新前のデータセット (oldData) の差分を比較することで、エンティティを細かく変更することができます。

A-Frameは、コンポーネントのライフサイクルの最初と、コンポーネントのデータが変更される(例えば、setAttributeの結果として)たびに、.update()を呼び出します。update ハンドラは、しばしば this.data を使用してエンティティを変更します。update ハンドラは、その最初の引数を通じてコンポーネントのデータの以前の状態にアクセスできます。コンポーネントの以前のデータを使用して、どのプロパティが変更されたかを正確に伝えることで、きめ細かな更新を行うことができます。

例えば、visible コンポーネントのupdateは、エンティティの可視性を設定します。

AFRAME.registerComponent('visible', {
  /**
   * this.el is the entity element.
   * this.el.object3D is the three.js object of the entity.
   * this.data is the component's property or properties.
   */
  update: function (oldData) {
    this.el.object3D.visible = this.data;
  }
  // ...
});

# .remove ()

.remove() は、コンポーネントがエンティティから切り離されるたびに呼び出されます。エンティティはコンポーネントのremoveハンドラを呼び出すことができます。

  • コンポーネントがremoveAttributeによってエンティティから削除されたとき。
  • コンポーネントがシーンから削除されたとき。(例:removeChild

remove ハンドラは、次のような用途によく使われます。

  • コンポーネントによるエンティティへの変更をすべて削除、取り消し、またはクリーンアップする。
  • イベントリスナーを切り離す。

たとえば、light コンポーネントが削除されると、light コンポーネントは、以前にエンティティに設定した light オブジェクトを削除し、シーンから削除されます。

AFRAME.registerComponent('light', {
  // ...
  remove: function () {
    this.el.removeObject3D('light');
  }
  // ...
});

# .tick (time, timeDelta)

.tick () は、シーンのレンダリングループの各ティックまたはフレームで呼び出されます。シーンは、コンポーネントのtickハンドラを呼び出します。

  • レンダリングループ内の各フレームごとに呼び出されます。
  • 1秒間に60回から120回の割合で呼び出されます。
  • エンティティまたはシーンが一時停止されていない場合に呼び出されます(例:インスペクタが開かれている)。
  • エンティティがまだシーンに接続されている場合に呼び出されます。

tick ハンドラは、次のような用途によく使われます。

  • 各フレームまたは一定間隔でエンティティを継続的に変更する時。
  • 条件をポーリングする時。

tick ハンドラには、ミリ秒単位(time)でのシーンのグローバルな稼働時間と、最後のフレームからの時間差分をミリ秒単位(timeDelta)で提供されます。これらはインターポーリングに使用したり、設定された間隔でティックハンドラの一部だけを実行するために使用することができます。

例えば、tracked controls コンポーネントは、コントローラのアニメーションを進行させ、コントローラの位置と回転ボタンが押されたことをチェックします。

AFRAME.registerComponent('tracked-controls', {
  // ...
  tick: function (time, timeDelta) {
    this.updateMeshAnimation();
    this.updatePose();
    this.updateButtons();
  }
  // ...
});

# .tock (time, timeDelta, camera)

tickメソッドと機能が同じですが、シーンがレンダリングされた後に呼び出されます。

tock ハンドラは、ポストプロセッシングエフェクトのように、ヘッドセットにプッシュされる前に描画されたシーンにアクセスする必要があるロジックを実行するために使用されます。

# .pause ()

.pause () は、エンティティまたはシーンが一時停止するときに呼び出されます。エンティティは、コンポーネントのpauseハンドラを呼び出すことができます。

  • コンポーネントが削除される前、removeハンドラが呼ばれる前。
  • エンティティがEntity.pause()で一時停止されたとき。
  • Scene.pause () でシーンが一時停止されたとき (例: インスペクタが開かれたとき)。

The pause

ハンドラは、しばしば次のように使用されます。

イベントリスナーを削除する。
動的な動作の可能性を取り除く。

例えば、サウンドコンポーネントは、音声を一時停止し、イベント時に音声を再生していたイベントリスナーを削除します。

AFRAME.registerComponent('sound', {
  // ...
  pause: function () {
    this.pauseSound();
    this.removeEventListener();
  }
  // ...
});

# .play ()

.play() は、エンティティまたはシーンが再開されたときに呼び出されます。エンティティは、コンポーネントのplayハンドラを呼び出すことができます。

  • コンポーネントが最初にアタッチされたとき、updateハンドラが呼び出された後。
  • エンティティが一時停止され、その後Entity.play()で再開されたとき。
  • シーンが一時停止され、Scene.play()で再開されたとき。

play ハンドラは、次のような用途によく使われます。

  • イベントリスナーを追加する。

例えば、サウンドコンポーネントは、サウンドを再生し、イベント時にサウンドを再生するイベントリスナーを更新します。

AFRAME.registerComponent('sound', {
  // ...
  play: function () {
    if (this.data.autoplay) { this.playSound(); }
    this.updateEventListener();
  }
  // ...
});

# .updateSchema (data)

.updateSchema ()が定義されている場合、スキーマを動的に変更する必要があるかどうかを確認するために、更新のたびに呼び出されます。

updateSchema ハンドラは、次のような用途によく使われます。

  • スキーマを動的に更新または拡張する場合。通常はプロパティの値に応じます。

たとえば、ジオメトリ コンポーネント では、primitive プロパティが変更されたかどうかをチェックして、異なるタイプのジオメトリ用にスキーマを更新するかどうかを決定します。

AFRAME.registerComponent('geometry', {
  // ...
  updateSchema: (newData) {
    if (newData.primitive !== this.data.primitive) {
      this.extendSchema(GEOMETRIES[newData.primitive].schema);
    }
  }
  // ...
});

# 定義プロパティ

# 依存関係

dependencies は、あるコンポーネントが他のコンポーネントに依存している場合に、コンポーネントの初期化の順序を制御することを可能にします。dependencies 配列で指定したコンポーネント名は、現在のコンポーネントを初期化する前に左から右へと初期化されます。依存関係が他の依存コンポーネントを持つ場合、それらの他の依存コンポーネントは、同じ方法で順序付けされます。

// Initializes last.
AFRAME.registerComponent('a', {
  dependencies: ['b']
});

// Initializes second.
AFRAME.registerComponent('b', {
  dependencies: ['c']
});

// Initializes first.
AFRAME.registerComponent('c', {});

# multiple

multipleフラグは、あるコンポーネントがエンティティ上にそれ自身の複数のインスタンスを持つことを可能にします。multipleはデフォルトでfalseに設定されているので、エンティティはコンポーネントのインスタンスを1つだけ持つことができます。例えば、エンティティは1つのジオメトリコンポーネントしか持つことができません。

しかし、multipletrueに設定されているコンポーネントは、複数のインスタンスを持つことができます。

AFRAME.registerComponent('foo', {
  multiple: true,
  // ...
});

DOMにおいては、ダブルアンダースコアとID(__<ID>)の接尾辞を与えることで、コンポーネントのインスタンスを区別することができます。例えば、サウンドコンポーネントのインスタンスを複数添付する場合には以下のようにします。

<a-scene>
  <a-entity
    sound="src: url(sound.mp3)"
    sound__beep="src: url(beep.mp3)"
    sound__boop="src: url(boop.mp3)"
  ></a-entity>
</a-scene>

コンポーネントのライフサイクルハンドラーメソッドから、this.idでコンポーネントのインスタンスを区別することができます。 コンポーネントのインスタンスがfoo__barと設定されている場合、this.id"bar"となります。

AFRAME.registerComponent('foo', {
  multiple: true,

  update: function () {
    console.log('This component instance has the ID', this.id);
  }
});

If we're doing a setObject3D(), we'll usually want to use this.attrName. If a component instance is set with foo__bar, then this.attrName would be foo__bar. This gives us a namespace and an ID to set an object3D on the entity's object3DMap:

setObject3D()を行う場合、通常はthis.attrNameを使いたいところです。もし、コンポーネントのインスタンスにfoo__barが設定されていれば、this.attrNamefoo__barとなります。これで、エンティティのobject3DMapobject3Dを設定するための名前空間とIDが付与されます。

AFRAME.registerComponent('foo', {
  multiple: true,

  update: function () {
    // An object3D will be set using `foo__bar` as the key.
    this.el.setObject3D(this.attrName, new THREE.Mesh());
  }
});

# イベント

eventオブジェクトは、コンポーネントのライフサイクルの中で適切なタイミングでバインドされ、自動的にアタッチおよびデタッチされるイベントハンドラを便利に定義することができます。

  • .play()でアタッチされる
  • .pause().remove()で切り離されます。

eventを使用することで、エンティティやシーンが一時停止したり、コンポーネントが切り離されたときに、イベントハンドラが適切に自分自身をクリーンにすることが保証されます。 コンポーネントのイベントハンドラを手動で登録し、適切にデタッチしなかった場合、そのコンポーネントが存在しなくなった後でもイベントハンドラが起動することがあります。

AFRAME.registerComponent('foo', {
  events: {
    click: function (evt) {
      console.log('This entity was clicked!');
      this.el.setAttribute('material', 'color', 'red');
    }
  }
});

# コンポーネント プロトタイプメソッド

# .flushToDOM ()

文字列化のCPU時間を節約するため、A-Frameはデバッグモードでは、実際のDOMにあるコンポーネントのシリアル化された表現のみを更新します。 flushToDOM()を呼び出すと、コンポーネントのデータを手動でシリアライズし、DOMを更新します。

document.querySelector('[geometry]').components.geometry.flushToDOM();

コンポーネントとDOMのシリアル化 を読む。

# コンポーネントのメンバーおよびメソッドへのアクセス

コンポーネントのメンバーやメソッドは、エンティティを通して.componentsオブジェクトからアクセスすることができます。エンティティのコンポーネントマップからコンポーネントを探せば、そのコンポーネントの内部にアクセスすることができます。この例のコンポーネントを考えてみましょう。

AFRAME.registerComponent('foo', {
  init: function () {
    this.bar = 'baz';
  },

  qux: function () {
    // ...
  }
});

barメンバーとquxメソッドにアクセスしてみましょう。

var fooComponent = document.querySelector('[foo]').components.foo;
console.log(fooComponent.bar);
fooComponent.qux();