当前位置: 首页 > 文档资料 > A-Frame 中文文档 >

组件(Component)

优质
小牛编辑
124浏览
2023-12-01

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>

propsimage 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),该属性类型将调用getElementByIdgetAttribute('src')来返回一个URL。这个asset属性类型可能会也可能不会改变处理XHRs或直接返回媒体元素(比如,<img>元素)。‘’
audioasset属性类型的解析类似。可能会给A-Frame查看器用于呈现一个音频资源。‘’
boolean将字符串解析为布尔值(i.e., "false"为false,否则为true)。false
color当前不进行任何解析。主要是给A-Frame检查器呈现一个颜色选择器。#FFF
int调用parseInt,取整操作(比如,"124.5"124)。0
mapasset属性类型的解析类似。可能会给A-Frame查看器用于呈现一个纹理资源。‘’
modelasset属性类型的解析类似。可能会给A-Frame查看器用于呈现一个模型资源。‘’
number调用parseFloat,把字符串转为数值(比如,"124.5"124.5')。0
selector调用querySelector(比如,"#box"<a-entity>)。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: 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交互。

methodsimage 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.elentity对象的引用,作为一个HTML元素。
this.el.sceneElscene对象的引用,作为一个HTML元素。
this.id如果该组件可以拥有.init ()

.init ()在组件生命周期开始时被调用一次。一个实体可以调用该组件的init处理器:

  • 当这个组件被静态的设置在HTML文件中的实体中,并且页面已加载。
  • 当一个组件通过setAttribute设置到一个场景中的实体上。
  • 当组件被设置在一个独立的实体,而实体然后连接到现场通过appendChild.

init方法通常用于:

  • 设置初始状态和变量
  • 绑定方法
  • 附加事件侦听器

比如,一个光标组件的init方法将设置状态(state)变量,绑定方法并添加事件侦听器如下:

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完成修改实体的大部分工作。
  • 每当一个或多个组件属性发生变化时被调用来修改实体。

对实体细粒度的修改可以通过在更新之前比对当前数据集(this.data)和原有数据集(oldData)来完成。

A-Frame在组件的生命周期的开始或者每一个时间组件的数据变化时调用.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处理器:

  • 在场景渲染循环的每一帧上。
  • 按每秒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();