组件(Component)
在entity-component-system pattern里,组件是一个可重用和模块化的数据块,我们将其插入到一个实体中,以添加外观、行为和/或功能。
在A-Frame中,组件修改场景中的三维对象实体。我们将组件组合在一起构建复杂对象。组件让我们把three.js和JS代码封装进模块里,以便于在HTML以声明的方式进行使用。
作为一个抽象的类比,如果我们把智能手机定义为一个实体,我们就可以使用组件来给它添加外观(颜色,形状),确定其行为(在来电时振动,在低电量时自动关闭),以及添加功能(照相机、屏幕)。
组件大体上类似于CSS。好比CSS规则用来定义元素的外观,组件属性用来定义实体的外观、行为和功能。
组件的HTML形式
组件以一个或多个组件属性的形式保存一组数据。组件使用这些数据来修改实体。考虑一个马达(engine)组件,我们可以定义其属性,如:马力(horsepower)或汽缸(cylinders)。
HTML属性表示组件名称,这些属性的值表示组件数据。
单属性组件
如果组件是一个单属性(single-property)组件,这意味着它的数据由单值组成,然后在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> |
多属性组件
如果组件是一个多属性(multi-property)组件,这意味着它的数据由多个属性和值组成,然后在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)
我们必须在场景中使用组件之前注册它。也就是在HTML文件中,组件应该出现在<a-scene>
之前。
{string} name
- 组件名称。通过HTML属性名表示的组件公开API。{Object} definition
- 组件定义。包含模式(schema)和生命周期处理器。
// Registering component in foo-component.jsAFRAME.registerComponent('foo', { schema: {}, init: function () {}, update: function () {}, tick: function () {}, remove: function () {}, pause: function () {}, play: function () {}}); |
<!-- Usage of `foo` component. --><html> <head> <script src="aframe.min.js"></script> <script src="foo-component.js"></script> </head> <body> <a-scene> <a-entity foo></a-entity> </a-scene> </body></html> |
模式(Schema)
模式是一个用来定义和描述组件属性的对象。模式的键是属性的名称,模式的值定义属性的类型和值(如果 是多属性组件):
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
Property Types
属性类型主要定义schema如何为每个属性解析来自DOM的传入数据。解析后的数据将可以通过组件原型的data
属性来访问。下面是A-Frame的内置属性类型:
属性类型 | 描述 | 缺省值 |
---|---|---|
array | 将逗号分隔的值解析为数组。(i.e., "1, 2, 3" to ['1', '2', '3']) . | [] |
asset | 用来指向资源的URL。可以以url(<url>) 的形式解析URL字符串。如果值是元素id选择器(比如,#texture ),该属性类型将调用getElementById 和getAttribute('src') 来返回一个URL。这个asset 属性类型可能会也可能不会改变处理XHRs或直接返回媒体元素(比如,<img> 元素)。 | ‘’ |
audio | 和asset 属性类型的解析类似。可能会给A-Frame查看器用于呈现一个音频资源。 | ‘’ |
boolean | 将字符串解析为布尔值(i.e., "false" 为false,否则为true)。 | false |
color | 当前不进行任何解析。主要是给A-Frame检查器呈现一个颜色选择器。 | #FFF |
int | 调用parseInt ,取整操作(比如,"124.5" 为124 )。 | 0 |
map | 和asset 属性类型的解析类似。可能会给A-Frame查看器用于呈现一个纹理资源。 | ‘’ |
model | 和asset 属性类型的解析类似。可能会给A-Frame查看器用于呈现一个模型资源。 | ‘’ |
number | 调用parseFloat ,把字符串转为数值(比如,"124.5" 为124.5' )。 | 0 |
selector | 调用querySelector (比如,"#box" 为<a-entity> )。 | 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: 0schema: {type: 'string'} // default: ''schema: {type: 'vec3'} // default: {x: 0, y: 0, z: 0} |
Custom Property Type
我们还可以通过提供一个parse
函数来定义我们自己的属性类型或解析器来代替一种type
:
schema: { // Parse slash-delimited string to an array (比如,`foo="myProperty: a/b"` to `['a', 'b']`). myProperty: { default: [], parse: function (value) { return value.split('/'); } }} |
Single-Property Schema
组件可以是单属性组件(由一个匿名值组成)或多属性组件(由多个命名值组成)。A-Frame将根据模式的结构来推断出组件是单属性还是多属性。
一个单属性组件的模式包含type
和/或default
键,并且模式的值是普通类型值而不是对象:
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
方法概览
方法 | 描述 |
---|---|
init | 初始化组件时调用一次。用于设置初始状态和实例化变量。 |
update | 在组件初始化和任何组件属性更新(例如,通过setAttribute)修改属性)时被调用来更新该实体。 |
remove | 在组件被从实体中删除时(比如,通过removeAttribute)或者当实体从场景中分离时被调用。用于撤消以前所有的实体修改。 |
tick | 在每个场景渲染循环被调用。用于连续的改变或检查。 |
play | 每当场景或实体播放来添加任意背景或动态行为时被调用。组件初始化时也会被调用一次。用于启动或恢复动态行为。 |
pause | 每当场景或实体暂停来删除任意背景或动态行为时被调用。当组件从实体中移除或实体脱离场景时也会被调用。用于暂停动态行为。 |
updateSchema | 当组件的任意属性更新时被调用。可用来动态修改模式(schema)。 |
组件原型属性
在这些方法里,我们可以通过this
来访问组件的原型:
属性 | 描述 | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
this.data | 解析出来的组件属性,通过模式(schema)的默认值、混合(mixins)和实体的属性计算而来。 | |||||||||||||||
this.el | entity对象的引用,作为一个HTML元素。 | |||||||||||||||
this.el.sceneEl | scene对象的引用,作为一个HTML元素。 | |||||||||||||||
this.id | 如果该组件可以拥有.init ()
比如,一个光标组件的
|
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
处理器:
- 在场景渲染循环的每一帧上。
- 按每秒60到120次的顺序。
- 如果实体或场景为被暂停(比如,查看器打开)。
- 如果实体仍然在场景中。
tick
处理器通常用于:
- 连续修改每个帧或时间间隔上的实体。
- 条件轮询。
tick
处理器提供场景全局的正常运行毫秒数(time
)以及最后一帧后毫秒的时间差(timeDelta
)。这些可以用于在指定间隔上插值或只运行部分的tick
处理器。
比如,tracked controls组件将处理控制器的动画,更新控制器的位置和旋转,以及检测按钮动作。
AFRAME.registerComponent('tracked-controls', { // ... tick: function (time, timeDelta) { this.updateMeshAnimation(); this.updatePose(); this.updateButtons(); } // ...}); |
.pause ()
.pause ()
当实体或场景暂停时调用。实体可以调用组件的pause
处理器:
- 在移除组件之前,在
remove
处理器被调用之前。 - 当实体被通过
Entity.pause ()
方法暂停时。 - 当场景被通过
Scene.pause ()
方法暂停时(比如,查看器被打开)。
pause
处理器通常用于:
- 删除事件侦听器。
- 停止任何的动态行为。
比如,sound组件将暂停声音播放并移除关联的事件监听器:
AFRAME.registerComponent('sound', { // ... pause: function () { this.pauseSound(); this.removeEventListener(); } // ...}); |
.play ()
.play ()
当实体或场景恢复时调用。实体可以调用一个组件的play
处理器:
- 当组件第一次被附加后,在
update
处理器调用之后。 - 当实体被暂停,但随后通过
Entity.play ()
恢复。 - 当场景被暂停,但随后通过
Scene.play ()
恢复。
play
处理器通常用于:
- 添加事件侦听器。
比如,sound组件将播放声音并更新用来在事件中播放声音的事件侦听器:
AFRAME.registerComponent('sound', { // ... play: function () { if (this.data.autoplay) { this.playSound(); } this.updateEventListener(); } // ...}); |
.updateSchema (data)
.updateSchema ()
,如果定义了这个方法,将在每次更新时调用,以检查模式是否需要被动态修改。
updateSchema
处理器通常用于:
- 动态更新或扩展模式,这通常取决于属性的值。
比如,geometry组件检查primitive
属性是否被更改,以决定是否为不同的几何模型来更新模式:
AFRAME.registerComponent('geometry', { // ... updateSchema: (newData) { if (newData.primitive !== this.data.primitive) { this.extendSchema(GEOMETRIES[newData.primitive].schema); } } // ...}); |
定义属性
dependencies
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
,组件只有单个实例(单例模式)。例如,一个实体只能有一个几何(geometry)组件。
但是如果一个组件的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); }}); |
如果我们执行一个setObject3D()
,我们通常将希望使用this.attrName
。如果一个组件的实例被设置为foo__bar
,那么this.attrName
将是foo__bar
。这给了我们一个命名空间和一个ID来设置实体object3DMap
中的object3D
:
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()); }}); |
组件原型方法
.flushToDOM ()
为了节省字符串化所用CPU时间,A-Frame只会在调试模式下更新组件在实际DOM中的序列化表示。调用flushToDOM ()
将手动序列化组件数据并更新DOM:
document.querySelector('[geometry]').components.geometry.flushToDOM(); |
阅读更多:component-to-DOM序列化。
访问组件的成员和方法
组件的成员和方法可以从实体中通过.components
对象来访问。从实体的组件映射表(map)中查找组件,我们将可以访问组件的内部。考虑下面这个组件实例:
AFRAME.registerComponent('foo', { init: function () { this.bar = 'baz'; }, qux: function () { // ... }}); |
我们可以访问bar
成员和qux
方法:
var fooComponent = document.querySelector('[foo]').components.foo;console.log(fooComponent.bar);fooComponent.baz(); |