Component
エンティティコンポーネントシステムパターンでは、コンポーネントは再利用可能でモジュール化されたデータの塊で、外観、動作、機能性を追加するためにエンティティに差し込まれます。
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>
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 )の場合、このプロパティタイプはgetElementById とgetAttribute('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
を呼び出し、 NodeList
を Array
にコンバートします (例:, ".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}
# カスタムプロパティタイプ
また、 parse
と stringify
関数を 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と協調します。
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つのジオメトリコンポーネントしか持つことができません。
しかし、multiple
がtrue
に設定されているコンポーネントは、複数のインスタンスを持つことができます。
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.attrName
はfoo__bar
となります。これで、エンティティのobject3DMap
にobject3D
を設定するための名前空間と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();