一、JavaScript、事件、DOM API
由于 A-Frame 只是 HTML,因此我们可以使用 JavaScript 和 DOM API 来控制场景及其实体,就像我们在普通 Web 开发中所做的那样。
场景中的每个元素,甚至诸如 <a-box>
或 <a-sky>
之类的元素,都是实体(表示为 <a-entity>
)。 A-Frame 修改了 HTML 元素原型,为某些 DOM API 添加一些额外的行为,以将它们定制为 A-Frame。请参阅实体 API 文档以获取有关下面讨论的大多数 API 的参考。
1.何处放置JavaScript 代码?
重要提示:在我们介绍使用 JavaScript 和 DOM API 的不同方法之前,我们规定将 JavaScript 代码封装在 A-Frame 组件中。组件模块化代码,使逻辑和行为从 HTML 中可见,并确保代码在正确的时间执行(例如,在场景和实体附加并初始化之后)。作为最基本的示例,在 <a-scene>
之前注册 console.log
组件:
AFRAME.registerComponent('log', {
schema: {type: 'string'},
init: function () {
var stringToLog = this.data;
console.log(stringToLog);
}
});
注册后,使用 HTML 中的组件:
<a-scene log="Hello, Scene!">
<a-box log="Hello, Box!"></a-box>
</a-scene>
组件封装了我们所有的代码,使其可重用、声明性和可共享。不过,如果我们只是在运行时闲逛,我们可以使用浏览器的开发人员工具控制台在场景中运行 JavaScript。
不要尝试将与 A-Frame相关的 JavaScript 放入 <a-scene>
之后的原始 <script>
标记中,就像我们使用传统的 2D 脚本一样。如果这样做,我们必须采取特殊措施来确保代码在正确的时间运行(请参阅在场景上运行内容脚本)。
2.如何查询和遍历获取实体?
DOM 作为场景图的美妙之处在于,标准 DOM 通过 .querySelector()
和 .querySelectorAll()
提供了用于遍历、查询、查找和选择的实用程序。最初受到 jQuery 选择器的启发,我们可以在 MDN 上了解查询选择器。
让我们运行一些示例查询选择器。以下面的场景为例。
<html>
<a-scene>
<a-box id="redBox" class="clickable" color="red"></a-box>
<a-sphere class="clickable" color="blue"></a-sphere>
<a-box color="green"></a-box>
<a-entity light="type: ambient"></a-entity>
<a-entity light="type: directional"></a-entity>
</a-scene>
</html>
(1)使用.querySelector()
如果我们只想获取一个元素,我们可以使用 .querySelector()
返回一个元素。让我们获取场景元素:
var sceneEl = document.querySelector('a-scene');
请注意,如果我们在组件中工作,我们已经拥有了对场景元素的引用,而无需查询。所有实体都引用其场景元素:
AFRAME.registerComponent('foo', {
init: function () {
console.log(this.el.sceneEl); // Reference to the scene element.
}
});
如果元素有 ID,我们可以使用 ID 选择器(即 #<ID>
)。让我们获取有 ID 的红色盒子。在我们对整个文档进行查询选择器之前。在这里,我们将在场景范围内执行查询选择器。使用查询选择器,我们可以将查询范围限制在任何元素内:
var sceneEl = document.querySelector('a-scene');
console.log(sceneEl.querySelector('#redBox'));
// <a-box id="redBox" class="clickable" color="red"></a-box>
(2)使用.querySelectorAll()
如果我们想获取一组元素,我们可以使用 .querySelectorAll()
返回一个元素数组。我们可以跨元素名称进行查询:
console.log(sceneEl.querySelectorAll('a-box'));
// [
// <a-box id="redBox" class="clickable" color="red"></a-box>,
// <a-box color="green"></a-box>
// ]
我们可以查询具有类选择器的类的元素(即 .<CLASS_NAME>
)。让我们获取每个具有 clickable
类的实体:
console.log(sceneEl.querySelectorAll('.clickable'));
// [
// <a-box id="redBox" class="clickable" color="red"></a-box>
// <a-sphere class="clickable" color="blue"></a-sphere>
// ]
我们可以使用属性选择器(即 [<ATTRIBUTE_NAME>]
)查询包含属性(或者在本例中为组件)的元素。让我们抓住每个有光的实体:
console.log(sceneEl.querySelectorAll('[light]'));
// [
// <a-entity light="type: ambient"></a-entity>
// <a-entity light="type: directional"></a-entity>
// ]
(3)遍历.querySelectorAll() 中的实体
如果我们使用 .querySelectorAll()
获取一组实体,我们可以使用 for
循环遍历它们。让我们用 *
循环场景中的每个元素。
var els = sceneEl.querySelectorAll('*');
for (var i = 0; i < els.length; i++) {
console.log(els[i]);
}
(4)性能注意事项
避免在每帧都会调用的 tick
和 tock
函数中使用 .querySelector
和 .querySelectorAll
,因为循环 DOM 确实需要一些时间检索实体。相反,保留实体的缓存列表,事先调用查询选择器,然后循环遍历它。
AFRAME.registerComponent('query-selector-example', {
init: function () {
this.entities = document.querySelectorAll('.box');
},
tick: function () {
// Don't call query selector in here, query beforehand.
for (let i = 0; i < this.entities.length; i++) {
// Do something with entities.
}
}
});
3.如何修改场景图?
使用 JavaScript 和 DOM API,我们可以像使用普通 HTML 元素一样动态添加和删除实体。
(1)使用 .createElement() 创建实体
要创建实体,我们可以使用 document.createElement
。这将为我们提供一个空白实体:
var el = document.createElement('a-entity');
但是,在我们将其附加到场景之前,该实体不会被初始化或成为场景的一部分。
(2)使用 .appendChild() 添加实体
要将实体添加到 DOM,我们可以使用 .appendChild(element)
。具体来说,我们想将其添加到我们的场景中。我们查找场景,创建实体,并将实体附加到场景中。
var sceneEl = document.querySelector('a-scene');
var entityEl = document.createElement('a-entity');
// Do `.setAttribute()`s to initialize the entity.
sceneEl.appendChild(entityEl);
请注意, .appendChild()
是浏览器中的异步操作。在实体完成向 DOM 的追加之前,我们无法对实体执行许多操作(例如调用 .getAttribute()
)。如果我们需要查询刚刚附加的实体上的属性,我们可以监听实体上的 loaded
事件,或者将逻辑放在 A-Frame 组件中,以便一旦添加就执行准备好:
var sceneEl = document.querySelector('a-scene');
AFRAME.registerComponent('do-something-once-loaded', {
init: function () {
// This will be called after the entity has properly attached and loaded.
console.log('I am ready!');
}
});
var entityEl = document.createElement('a-entity');
entityEl.setAttribute('do-something-once-loaded', '');
sceneEl.appendChild(entityEl);
(3)使用 .removeChild() 删除实体
要从 DOM 中删除实体,从而从场景中删除实体,我们从父元素中调用 .removeChild(element)
。如果我们有一个实体,我们必须要求其父级( parentNode
)删除该实体。
entityEl.parentNode.removeChild(entityEl);
4.如何操纵实体组件?
空白实体不执行任何操作。我们可以通过添加组件、配置组件属性和删除组件来修改实体。
(1)【增】使用 .setAttribute() 添加组件
要添加组件,我们可以使用 .setAttribute(componentName, data)
。让我们向实体添加一个几何组件。
entityEl.setAttribute('geometry', {
primitive: 'box',
height: 3,
width: 1
});
或者添加社区物理组件:
entityEl.setAttribute('dynamic-body', {
shape: 'box',
mass: 1.5,
linearDamping: 0.005
});
与普通的 HTML .setAttribute()
不同,实体的 .setAttribute()
得到了改进,可以接受各种类型的参数(例如对象),或者能够更新组件的单个属性。
(2)【删】使用 .removeAttribute() 删除组件
要从实体中删除或分离组件,我们可以使用 .removeAttribute(componentName)
。让我们从相机实体中删除默认的 wasd-controls
:
var cameraEl = document.querySelector('[camera]');
cameraEl.removeAttribute('wasd-controls');
(3)【改】使用 .setAttribute() 更新组件
要更新组件,我们还使用 .setAttribute()
。更新组件有多种形式。
1)更新单属性组件的属性
让我们更新位置组件的属性,这是一个单属性组件。我们可以传递一个对象或一个字符串。稍微推荐传递一个对象,这样 A-Frame 就不必解析字符串。
entityEl.setAttribute('position', {x: 1, y: 2, z: -3});
// Read on to see why `entityEl.object3D.position.set(1, 2, -3)` is preferred though.
2)更新多属性组件的单个属性
让我们更新材质组件的单个属性,即多属性组件。我们通过向 .setAttribute()
提供组件名称、属性名称和属性值来实现此目的:
entityEl.setAttribute('material', 'color', 'red');
3)更新多属性组件的多个属性
让我们一次更新光组件的多个属性,这是一个多属性组件。我们通过向 .setAttribute()
提供组件名称和属性对象来做到这一点。我们将更改灯光的颜色和强度,但保持类型不变:
// <a-entity light="type: directional; color: #CAC; intensity: 0.5"></a-entity>
entityEl.setAttribute('light', {color: '#ACC', intensity: 0.75});
// <a-entity light="type: directional; color: #ACC; intensity: 0.75"></a-entity>
4)更新 position 、 rotation 、 scale 和 visible 。
作为特殊情况,为了获得更好的性能、内存和对实用程序的访问,我们建议修改 position
、 rotation
、 scale
和 visible
直接在 Three.js 级别通过实体的 Object3D 而不是通过 .setAttribute
:
// Examples for position.
entityEl.object3D.position.set(1, 2, 3);
entityEl.object3D.position.x += 5;
entityEl.object3D.position.multiplyScalar(5);
// Examples for rotation.
entityEl.object3D.rotation.y = THREE.MathUtils.degToRad(45);
entityEl.object3D.rotation.divideScalar(2);
// Examples for scale.
entityEl.object3D.scale.set(2, 2, 2);
entityEl.object3D.scale.z += 1.5;
// Examples for visible.
entityEl.object3D.visible = false;
entityEl.object3D.visible = true;
这让我们可以跳过 .setAttribute
开销,而是为最常更新的组件进行简单的属性设置。在进行例如 entityEl.getAttribute('position');
操作时,仍然会反映 Three.js 级别的更新。
5)替换多属性组件的属性
让我们替换几何组件的所有属性,这是一个多属性组件。我们通过提供组件名称、 .setAttribute()
的属性对象以及指定破坏现有属性的标志来实现此目的。我们将用新属性替换几何体的所有现有属性:
// <a-entity geometry="primitive: cylinder; height: 4; radius: 2"></a-entity>
entityEl.setAttribute('geometry', {primitive: 'torusKnot', p: 1, q: 3, radiusTubular: 4}, true);
// <a-entity geometry="primitive: torusKnot; p: 1; q: 3; radiusTubular: 4"></a-entity>
(4)【查】使用 .getAttribute() 检索组件数据
我们可以通过 .getAttribute
获取实体的组件数据。 A-Frame 增强了 .getAttribute
返回值而不是字符串(例如,在大多数情况下返回对象,因为组件通常由多个属性组成,或者返回像 .getAttribute('visible')
这样的实际布尔值。通常, .getAttribute
将返回组件的内部数据对象,因此不要直接修改该对象:
// <a-entity geometry="primitive: sphere; radius: 2"></a-entity>
el.getAttribute('geometry');
// >> {"primitive": "sphere", "radius": 2, ...}
检索 position 和 scale:
执行 el.getAttribute('position')
或 el.getAttribute('scale')
将返回 Three.js Object3D 位置和比例属性,它们是 Vector3。请记住,修改这些对象将修改实际的实体数据。
这是因为 A-Frame 允许我们在 Three.js 层面修改位置、旋转、缩放、可见,并且为了让 .getAttribute
返回正确的数据,A-Frame 返回实际的 Three.js Object3D 对象。
对于 .getAttribute('rotation')
来说情况并非如此,因为无论好坏,A-Frame都使用度数而不是弧度。在这种情况下,将返回具有 x/y/z 属性的普通 JavaScript 对象。如果我们需要在较低级别上使用弧度,则可以通过 el.object3D.rotation
检索 Object3D Euler。
5.如何定义事件和事件监听器
通过 JavaScript 和 DOM,实体和组件可以通过一种简单的方式相互通信:事件和事件侦听器。事件是一种发出信号的方式,其他代码可以接收并响应该信号。
(1)使用 .emit() 发出事件
A-Frame 元素提供了一种使用 .emit(eventName, eventDetail, bubbles)
发出自定义事件的简单方法。例如,假设我们正在构建一个物理组件,我们希望该实体在与另一个实体发生碰撞时发出信号:
entityEl.emit('physicscollided', {collidingEntity: anotherEntityEl}, false);
然后代码的其他部分可以等待并侦听此事件并运行代码作为响应。我们可以通过事件详细信息作为第二个参数传递信息和数据。我们可以指定事件是否冒泡,这意味着父实体也会发出事件。所以代码的其他部分可以注册事件监听器。
(2)使用 .addEventListener() 添加事件监听器
与普通 HTML 元素一样,我们可以使用 .addEventListener(eventName, function)
注册事件侦听器。当监听器注册的事件被发出时,该函数将被调用并处理该事件。例如,继续前面的物理碰撞事件示例:
entityEl.addEventListener('physicscollided', function (event) {
console.log('Entity collided with', event.detail.collidingEntity);
});
当实体发出 physicscollided
事件时,将使用事件对象调用该函数。值得注意的是,在事件对象中,我们有事件详细信息,其中包含通过事件传递的数据和信息。
(3)使用 .removeEventListener() 删除事件监听器
与普通 HTML 元素一样,当我们想要删除事件侦听器时,可以使用 .removeEventListener(eventName, function)
。我们必须传递与注册侦听器相同的事件名称和函数。例如,继续前面的物理碰撞事件示例:
// We have to define this function with a name if we later remove it.
function collisionHandler (event) {
console.log('Entity collided with', event.detail.collidingEntity);
}
entityEl.addEventListener('physicscollided', collisionHandler);
entityEl.removeEventListener('physicscollided', collisionHandler);
(4)绑定事件监听器
默认情况下,Javascript 执行上下文规则将 this
绑定到任何独立函数的全局上下文 ( window
),这意味着这些函数将无法访问组件的 this
为了使组件的 this
可以在事件侦听器内访问,必须对其进行绑定。
您可以通过多种方式执行此操作:
- By过使用箭头函数来定义事件监听器。箭头函数自动绑定
this
this.el.addEventListener('physicscollided', (event) => {
console.log(this.el.id);
});
-
通过在组件的事件对象中定义事件侦听器(这也将自动处理添加和删除侦听器)。
-
通过创建另一个函数,这是该函数的绑定版本。
this.listeners = {
clickListener: this.clickListener.bind(this);
}
entityEl.addEventListener('click', this.listeners.clickListener);
6.注意事项
A-Frame 实体和原语以有利于性能的方式实现,因此某些 HTML API 可能无法按预期工作。例如,涉及值的属性选择器将不起作用,并且当实体的组件发生更改时,突变观察器不会触发更改。
二、使用Three.js进行开发
1.A-Frame 和 Three.js 场景图之间的关系
-
A-Frame 的
<a-scene>
与 Three.js 场景一对一映射。 -
A-Frame 的
<a-entity>
映射到一个或多个 Three.js 对象。 -
Three.js 的对象通过
.el
引用其 A-Frame 实体,该引用由 A-Frame 设置。
父子关系:
当 A-Frame 实体嵌套在父子关系中时,它们的 Three.js 对象也是如此。例如,以这个 A-Frame场景为例:
<a-scene>
<a-box>
<a-sphere></a-sphere>
<a-light></a-light>
</a-box>
</a-scene>
Three.js 场景图将对应如下:
THREE.Scene
THREE.Mesh
THREE.Mesh
THREE.Light
2.如何访问 Three.js API?
Three.js 可作为窗口上的全局对象使用:
console.log(THREE);
3.如何使用 Three.js 对象?
A-Frame 是在 Three.js 之上的抽象,但我们仍然在底层使用 Three.js 进行操作。 A-Frame 的元素有通向 Three.js 场景图的门。
(1)如何访问 Three.js 场景?
Three.js 场景可以通过 <a-scene>
元素作为 .object3D
进行访问:
document.querySelector('a-scene').object3D; // THREE.Scene
每个 A-Frame实体还通过 .sceneEl
引用 <a-scene>
:
document.querySelector('a-entity').sceneEl.object3D; // THREE.Scene
从组件中,我们通过其实体(即 this.el
)访问场景:
AFRAME.registerComponent('foo', {
init: function () {
var scene = this.el.sceneEl.object3D; // THREE.Scene
}
});
(2)如何访问实体的 Three.js 对象
每个 A-Frame实体(例如 <a-entity>
)都有自己的 THREE.Object3D
,更具体地说是包含不同类型的 Object3D
的 THREE.Group
。实体的根 THREE.Group
通过 .object3D
访问:
document.querySelector('a-entity').object3D; // THREE.Group
实体可以由多种类型的 Object3D
组成。例如,通过同时具有几何组件和灯光组件,实体可以既是 THREE.Mesh
又是 THREE.Light
:
<a-entity geometry light></a-entity>
组件在实体的根 THREE.Group
下添加网格和灯光。对网格和灯光的引用作为不同类型的 Three.js 对象存储在实体的 .object3DMap
中。
console.log(entityEl.object3DMap);
// {mesh: THREE.Mesh, light: THREE.Light}
但我们可以通过实体的 .getObject3D(name)
方法访问它们:
entityEl.getObject3D('mesh'); // THREE.Mesh
entityEl.getObject3D('light'); // THREE.Light
现在让我们看看这Three.js 对象是如何设置的。
(3)如何在实体上设置 Object3D?
在实体上设置 Object3D
将 Object3D
添加到实体的 Group
中,这使得新设置的 Object3D
成为 Three.js 的一部分场景。我们使用实体的 .setObject3D(name)
方法设置 Object3D
,其中名称表示 Object3D
的用途。
例如,要从组件内设置点光源:
AFRAME.registerComponent('pointlight', {
init: function () {
this.el.setObject3D('light', new THREE.PointLight());
}
});
// <a-entity light></a-entity>
我们将灯光设置为名称 light
。为了稍后访问它,我们可以使用实体的 .getObject3D(name)
方法,如前所述:
entityEl.getObject3D('light');
当我们在 A-Frame 实体上设置 Three.js 对象时,A-Frame 将通过 .el
从 Three.js 对象设置对 A-Frame 实体的引用:
entityEl.getObject3D('light').el; // entityEl
(4)如何从实体中删除 Object3D?
要从实体中删除 Object3D
,从而从 Three.js 场景中删除,我们可以使用实体的 .removeObject3D(name)
方法。回到我们的点光源示例,我们在组件分离时移除光源:
AFRAME.registerComponent('pointlight', {
init: function () {
this.el.setObject3D('light', new THREE.PointLight());
},
remove: function () {
// Remove Object3D.
this.el.removeObject3D('light');
}
});
4.如何进行坐标空间之间的变换?
每个物体和场景(世界)通常都有自己的坐标空间。父对象的位置、旋转和缩放变换将应用于其子对象的位置、旋转和缩放变换。考虑这个场景:
<a-entity id="foo" position="1 2 3">
<a-entity id="bar" position="2 3 4"></a-entity>
</a-entity>
从世界的参考点来看,foo 的位置为 (1,2,3),bar 的位置为 (3,5,7),因为 foo 的变换适用于 bar 的变换。从 foo 的参考点来看,foo 的位置为 (0, 0, 0),bar 的位置为 (2, 3, 4)。
通常我们会想要在这些参考点和坐标空间之间进行转换。上面是一个简单的例子,但我们可能想要执行一些操作,例如查找 bar 位置的世界空间坐标,或者将任意坐标转换为 foo 的坐标空间。在 3D 编程中,这些操作是通过矩阵完成的,但 Three.js 提供了帮助器,使它们变得更容易。
(1)本地坐标到世界坐标的转换
通常,我们需要在父 Object3D
上调用 .updateMatrixWorld ()
,但 Three.js 默认 Object3D.matrixAutoUpdate
为 true
。我们可以使用 Three.js 的 .getWorldPosition (vector)
和 .getWorldQuaternion (quaternion)
。
要获取 Object3D
的世界位置:
var worldPosition = new THREE.Vector3();
entityEl.object3D.getWorldPosition(worldPosition);
要获取 Object3D
的世界旋转:
var worldQuaternion = new THREE.Quaternion();
entityEl.object3D.getWorldQuaternion(worldQuaternion);
Three.js Object3D
有更多可用于本地到世界转换的函数:
-
.localToWorld (vector)
-
.getWorldDirection (vector)
-
.getWorldQuaternion (quaternion)
-
.getWorldScale (vector)
(2)世界坐标到局部坐标的转换
要获得从世界变换到对象局部空间的矩阵,请获取对象世界矩阵的逆矩阵。
var worldToLocal = new THREE.Matrix4().getInverse(object3D.matrixWorld)
然后我们可以将该 worldToLocal
矩阵应用于我们想要转换的任何内容:
anotherObject3D.applyMatrix(worldToLocal);
三、进行组件开发
A-Frame 实体组件框架的组件是 JavaScript 模块,可以混合、匹配和组合到实体上以构建外观、行为和功能。我们可以在 JavaScript 中注册组件并从 DOM 中以声明方式使用它。组件是可配置的、可重用的和可共享的。 A 框架应用程序中的大多数代码应该位于组件内。
建议在阅读本指南之前浏览一下组件 API 文档,因为该文档会更加简洁。请注意,组件应在 <a-scene>
之前定义,如下所示:
<html>
<head>
<script src="foo-component.js"></script>
</head>
<body>
<script>
// Or inline before the <a-scene>.
AFRAME.registerComponent('bar', {
// ...
});
</script>
<a-scene>
</a-scene>
</body>
</html>
我们将讨论编写组件的示例。这些示例将完成大部分琐碎的事情,但将演示数据流、API 和用法。要查看重要组件的示例,请参阅通过生态系统中的组件进行学习部分。
示例1: hello-world 组件
让我们从最基本的组件开始,以获得一个总体概念。当使用 .init()
处理程序附加组件的实体时,该组件将记录一条简单的消息。
(1)使用 AFRAME.registerComponent 注册组件
组件通过 AFRAME.registerComponent()
注册。我们传递组件的名称,该名称将用作组件在 DOM 中表示的 HTML 属性名称。然后我们传递组件定义,它是方法和属性的 JavaScript 对象。在定义中,我们可以定义生命周期处理程序方法。其中之一是 .init()
,当组件首次插入其实体时调用一次。
在下面的示例中,我们只让 .init()
处理程序记录一条简单的消息。
AFRAME.registerComponent('hello-world', {
init: function () {
console.log('Hello, World!');
}
});
(2)在HTML中使用
然后我们可以以声明方式使用 hello-world
组件作为 HTML 属性。
<a-scene>
<a-entity hello-world></a-entity>
</a-scene>
现在,在附加并初始化实体后,它将初始化我们的 hello-world
组件。组件的奇妙之处在于它们仅在实体准备好后才会被调用。我们不必担心等待场景或实体设置,它就会正常工作!如果我们检查控制台, Hello, World!
将在场景开始运行并且实体已附加后记录一次。
(3)在 JS 中使用组件
设置组件的另一种方法是使用 .setAttribute()
以编程方式设置它,而不是通过静态 HTML。场景元素也可以使用组件,让我们以编程方式在场景上设置 hello-world
组件:
document.querySelector('a-scene').setAttribute('hello-world', '');
示例2: log 组件
与 hello-world
组件类似,让我们制作一个 log
组件。它仍然只会执行 console.log
操作,但我们将使其能够执行 console.log
操作,而不仅仅是 Hello, World!
操作。我们的 log
组件将记录传入的任何字符串。我们将通过架构定义可配置属性来了解如何将数据传递给组件。
(1)使用Schema定义属性
模式定义其组件的属性。打个比方,如果我们将组件视为函数,那么组件的属性就像它的函数参数。属性具有名称(如果组件有多个属性)、默认值和属性类型。属性类型定义如果数据作为字符串(即来自 DOM)传递,则如何解析数据。
对于我们的 log
组件,让我们通过 schema
定义一个 message
属性类型。 message
属性类型将具有 string
属性类型并具有默认值 Hello, World!
:
AFRAME.registerComponent('log', {
schema: {
message: {type: 'string', default: 'Hello, World!'}
},
// ...
});
(2)在生命周期处理程序中使用属性数据
string
属性类型不会对传入数据进行任何解析,而是将其按原样传递给生命周期方法处理程序。现在让我们 console.log
message
属性类型。与 hello-world
组件一样,我们编写一个 .init()
处理程序,但这次我们不会记录硬编码字符串。组件的属性类型值可通过 this.data
获得。那么让我们记录 this.data.message
!
AFRAME.registerComponent('log', {
schema: {
message: {type: 'string', default: 'Hello, World!'}
},
init: function () {
console.log(this.data.message);
}
});
然后,我们可以从 HTML 将组件附加到实体。对于多属性组件,语法与内联 css 样式相同(属性名称/值对由 :
分隔,属性由 ;
分隔):
<a-scene>
<a-entity log="message: Hello, Metaverse!"></a-entity>
</a-scene>
(3)处理属性更新
到目前为止,我们仅使用 .init()
处理程序,该处理程序仅在组件生命周期开始时仅使用其初始属性调用一次。但组件的属性通常会动态更新。我们可以使用 .update()
处理程序来处理属性更新。
为了演示这一点,我们将让 log
组件仅在其实体发出事件时记录日志。首先,我们将添加一个 event
属性类型,用于指定组件应侦听哪个事件。
// ...
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'},
},
// ...
然后我们实际上会将所有内容从 .init()
处理程序移动到 .update()
处理程序。当附加组件时, .update()
处理程序也会在 .init()
之后立即调用。有时,我们的大部分逻辑都在 .update()
处理程序中,因此我们可以一次性初始化和处理更新,而无需重复代码。
我们想要做的是添加一个事件侦听器,该侦听器将在记录消息之前侦听事件。如果未指定 event
属性类型,我们将仅记录消息:
AFRAME.registerComponent('log', {
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'}
},
update: function () {
var data = this.data; // Component property values.
var el = this.el; // Reference to the component's entity.
if (data.event) {
// This will log the `message` when the entity emits the `event`.
el.addEventListener(data.event, function () {
console.log(data.message);
});
} else {
// `event` not specified, just log the message.
console.log(data.message);
}
}
});
现在我们已经添加了事件侦听器属性,让我们处理实际的属性更新。当 event
属性类型更改时(例如,由于 .setAttribute()
),我们需要删除以前的事件侦听器,并添加一个新的事件侦听器。
但要删除事件侦听器,我们需要对该函数的引用。因此,每当我们附加事件侦听器时,我们首先将函数存储在 this.eventHandlerFn
上。当我们通过 this
将属性附加到组件时,它们将在所有其他生命周期处理程序中可用。
AFRAME.registerComponent('log', {
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'}
},
init: function () {
// Closure to access fresh `this.data` from event handler context.
var self = this;
// .init() is a good place to set up initial state and variables.
// Store a reference to the handler so we can later remove it.
this.eventHandlerFn = function () { console.log(self.data.message); };
},
update: function () {
var data = this.data;
var el = this.el;
if (data.event) {
el.addEventListener(data.event, this.eventHandlerFn);
} else {
console.log(data.message);
}
}
});
现在我们已经存储了事件处理函数。每当 event
属性类型发生更改时,我们就可以删除事件侦听器。我们只想在 event
属性类型更改时更新事件侦听器。我们通过检查 this.data
与 .update()
处理程序提供的 oldData
参数来做到这一点:
AFRAME.registerComponent('log', {
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'}
},
init: function () {
var self = this;
this.eventHandlerFn = function () { console.log(self.data.message); };
},
update: function (oldData) {
var data = this.data;
var el = this.el;
// `event` updated.
if (data.event !== oldData.event) {
// Remove the previous event listener, if it exists.
if (oldData.event) {
el.removeEventListener(oldData.event, this.eventHandlerFn);
}
// Add listener for new event, if it exists.
if (data.event) {
el.addEventListener(data.event, this.eventHandlerFn);
}
}
if (!data.event) {
console.log(data.message);
}
}
});
现在让我们使用更新事件监听器来测试我们的组件。这是我们的场景:
<a-scene>
<a-entity log="event: anEvent; message: Hello, Metaverse!"></a-entity>
</a-scene>
让我们的实体发出事件来测试它:
var el = document.querySelector('a-entity');
el.emit('anEvent');
// >> "Hello, Metaverse!"
现在让我们更新事件来测试 .update()
处理程序:
var el = document.querySelector('a-entity');
el.setAttribute('log', {event: 'anotherEvent', message: 'Hello, new event!'});
el.emit('anotherEvent');
// >> "Hello, new event!"
(4)处理组件移除
让我们处理组件从实体中删除的情况(即 .removeAttribute('log')
)。我们可以实现 .remove()
处理程序,该处理程序在组件被删除时调用。对于 log
组件,我们删除该组件附加到实体的所有事件侦听器:
AFRAME.registerComponent('log', {
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'}
},
init: function () {
var self = this;
this.eventHandlerFn = function () { console.log(self.data.message); };
},
update: function (oldData) {
var data = this.data;
var el = this.el;
if (oldData.event && data.event !== oldData.event) {
el.removeEventListener(oldData.event, this.eventHandlerFn);
}
if (data.event) {
el.addEventListener(data.event, this.eventHandlerFn);
} else {
console.log(data.message);
}
},
/**
* Handle component removal.
*/
remove: function () {
var data = this.data;
var el = this.el;
// Remove event listener.
if (data.event) {
el.removeEventListener(data.event, this.eventHandlerFn);
}
}
});
现在让我们测试一下删除处理程序。让我们删除该组件并检查发出事件不再执行任何操作:
<a-scene>
<a-entity log="event: anEvent; message: Hello, Metaverse!"></a-entity>
</a-scene>
var el = document.querySelector('a-entity');
el.removeAttribute('log');
el.emit('anEvent');
// >> Nothing should be logged...
(5)允许组件的多个实例
让我们允许将多个 log
组件附加到同一实体。为此,我们使用 .multiple
标志启用多个实例。让我们将其设置为 true
:
AFRAME.registerComponent('log', {
schema: {
event: {type: 'string', default: ''},
message: {type: 'string', default: 'Hello, World!'}
},
multiple: true,
// ...
});
多实例组件的属性名称的语法采用 <COMPONENTNAME>__<ID>
形式,即带有 ID 后缀的双下划线。 ID 可以是我们选择的任何内容。例如,在 HTML 中:
<a-scene>
<a-entity log__helloworld="message: Hello, World!"
log__metaverse="message: Hello, Metaverse!"></a-entity>
</a-scene>
或者来自 JS:
var el = document.querySelector('a-entity');
el.setAttribute('log__helloworld', {message: 'Hello, World!'});
el.setAttribute('log__metaverse', {message: 'Hello, Metaverse!'});
在组件内,如果我们愿意,我们可以使用 this.id
和 this.attrName
来区分不同的实例。给定 log__helloworld
, this.id
将是 helloworld
,而 this.attrName
将是完整的 log__helloworld
。
我们就有了基本的 log
组件!
示例3: box 组件
作为一个不太简单的示例,让我们了解如何通过编写使用 Three.js 的组件来添加 3D 对象并影响场景图。为了理解这个想法,我们只需制作一个基本的 box
组件,它可以创建具有几何形状和材质的盒网格。
注意:这只是编写 Hello, World!
组件的 3D 等效项。如果我们真的想在实践中制作一个盒子,A-Frame 提供了几何形状和材料组件。
(1)Schema和 API
让我们从schema开始。该schema定义了组件的 API。我们将通过属性来配置 width
、 height
、 depth
和 color
。 width
、 height
和 depth
将是数字类型(即浮点数),默认值为 1 米。 color
类型将具有默认为灰色的颜色类型(即字符串):
AFRAME.registerComponent('box', {
schema: {
width: {type: 'number', default: 1},
height: {type: 'number', default: 1},
depth: {type: 'number', default: 1},
color: {type: 'color', default: '#AAA'}
}
});
稍后,当我们通过 HTML 使用该组件时,语法将如下所示:
<a-scene>
<a-entity box="width: 0.5; height: 0.25; depth: 1; color: orange"
position="0 0 -5"></a-entity>
</a-scene>
(2)创建Box网格
让我们从 .init()
创建 Three.js 盒子网格,稍后我们将让 .update()
处理程序处理所有属性更新。要在 Three.js 中创建一个盒子,我们将创建一个 THREE.BoxGeometry
。 BoxGeometry
等生成器现在创建一个 BufferGeometry。请参阅 Three.js 手册以了解如何创建自定义 BufferGeometry
。 (性能较差的 Geometry
类在 Three.js 或 A-Frame 的最新版本中不可用。)
我们还需要一个 THREE.MeshStandardMaterial
,最后一个 THREE.Mesh
。然后我们在实体上设置网格,以使用 .setObject3D(name, object)
将网格添加到 Three.js 场景图中:
AFRAME.registerComponent('box', {
schema: {
width: {type: 'number', default: 1},
height: {type: 'number', default: 1},
depth: {type: 'number', default: 1},
color: {type: 'color', default: '#AAA'}
},
/**
* Initial creation and setting of the mesh.
*/
init: function () {
var data = this.data;
var el = this.el;
// Create geometry.
this.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth);
// Create material.
this.material = new THREE.MeshStandardMaterial({color: data.color});
// Create mesh.
this.mesh = new THREE.Mesh(this.geometry, this.material);
// Set mesh on entity.
el.setObject3D('mesh', this.mesh);
}
});
现在让我们处理更新。如果与几何图形相关的属性(即 width
、 height
、 depth
)更新,我们将重新创建几何图形。如果与材质相关的属性(即 color
)更新,我们将直接更新材质。要访问网格并更新它,我们使用 .getObject3D('mesh')
。
AFRAME.registerComponent('box', {
schema: {
width: {type: 'number', default: 1},
height: {type: 'number', default: 1},
depth: {type: 'number', default: 1},
color: {type: 'color', default: '#AAA'}
},
init: function () {
var data = this.data;
var el = this.el;
this.geometry = new THREE.BoxGeometry(data.width, data.height, data.depth);
this.material = new THREE.MeshStandardMaterial({color: data.color});
this.mesh = new THREE.Mesh(this.geometry, this.material);
el.setObject3D('mesh', this.mesh);
},
/**
* Update the mesh in response to property updates.
*/
update: function (oldData) {
var data = this.data;
var el = this.el;
// If `oldData` is empty, then this means we're in the initialization process.
// No need to update.
if (Object.keys(oldData).length === 0) { return; }
// Geometry-related properties changed. Update the geometry.
if (data.width !== oldData.width ||
data.height !== oldData.height ||
data.depth !== oldData.depth) {
el.getObject3D('mesh').geometry = new THREE.BoxGeometry(data.width, data.height,
data.depth);
}
// Material-related properties changed. Update the material.
if (data.color !== oldData.color) {
el.getObject3D('mesh').material.color = new THREE.Color(data.color);
}
}
});
(3)删除Box网格
最后,我们将处理组件或实体何时被删除。在这种情况下,我们需要从场景中删除网格。我们可以使用 .remove()
处理程序和 .removeObject3D(name)
来做到这一点:
AFRAME.registerComponent('box', {
// ...
remove: function () {
this.el.removeObject3D('mesh');
}
});
这就完成了基本的 Three.js box
组件!实际上,Three.js 组件会做一些更有用的事情。在 Three.js 中可以完成的任何事情都可以包装在 A-Frame 组件中以使其具有声明性。因此,请查看 Three.js 功能和生态系统,看看您可以编写哪些组件!
示例4: follow 组件
让我们编写一个 follow
组件,告诉一个实体跟随另一个实体。这将演示 .tick()
处理程序的使用,该处理程序将在渲染循环的每一帧上运行的连续运行行为添加到场景中。这也将展示实体之间的关系。
(1)Schema和 API
首先,我们需要一个 target
属性来指定要遵循的实体。 A-Frame 有一个 selector
属性类型来完成这个任务,允许我们传入查询选择器并获取实体元素。我们还将添加一个 speed
属性(以 m/s 为单位)来指定实体应该跟随的速度。
AFRAME.registerComponent('follow', {
schema: {
target: {type: 'selector'},
speed: {type: 'number'}
}
});
稍后,当我们通过 HTML 使用该组件时,语法将如下所示:
<a-scene>
<a-box id="target-box" color="#5E82C5" position="-3 0 -5"></a-box>
<a-box follow="target: #target-box; speed: 1" color="#FF6B6B" position="3 0 -5"></a-box>
</a-scene>
(2)创建辅助向量
由于 .tick()
处理程序将在每一帧上被调用(例如每秒 90 次),因此我们希望确保其性能。我们不想做的一件事是在每个刻度上创建不必要的对象,例如 THREE.Vector3
对象。这将有助于导致垃圾收集暂停。由于我们需要使用 THREE.Vector3
进行一些向量运算,因此我们将在 .init()
处理程序中创建它一次,以便稍后重用它:
AFRAME.registerComponent('follow', {
schema: {
target: {type: 'selector'},
speed: {type: 'number'}
},
init: function () {
this.directionVec3 = new THREE.Vector3();
}
});
(3)使用 .tick() 处理程序定义行为
现在我们将编写 .tick()
处理程序,以便组件以所需的速度连续将实体移向目标。 A-Frame 将全局场景正常运行时间作为 time
并将自上一帧以来的时间作为 timeDelta
传递到 tick()
处理程序中(以毫秒为单位)。我们可以使用 timeDelta
来计算在给定速度的情况下实体在这一帧中应该向目标行进多远。
为了计算实体应该前进的方向,我们从目标实体的位置向量中减去实体的位置向量。我们可以通过 .object3D
访问实体的 Three.js 对象,并从那里访问位置向量 .position
。我们将方向向量存储在之前在 init()
处理程序中分配的 this.directionVec3
中。
然后,我们考虑要走的距离、所需的速度以及自上一帧以来已经过去的时间,以找到添加到实体位置的适当向量。我们用 .setAttribute
翻译实体,在下一帧中, .tick()
处理程序将再次运行。
完整的 .tick()
处理程序如下。 .tick()
很棒,因为它允许以一种简单的方式挂钩渲染循环,而无需实际引用渲染循环。我们只需要定义一个方法。按照下面的代码注释进行操作:
AFRAME.registerComponent('follow', {
schema: {
target: {type: 'selector'},
speed: {type: 'number'}
},
init: function () {
this.directionVec3 = new THREE.Vector3();
},
tick: function (time, timeDelta) {
var directionVec3 = this.directionVec3;
// Grab position vectors (THREE.Vector3) from the entities' three.js objects.
var targetPosition = this.data.target.object3D.position;
var currentPosition = this.el.object3D.position;
// Subtract the vectors to get the direction the entity should head in.
directionVec3.copy(targetPosition).sub(currentPosition);
// Calculate the distance.
var distance = directionVec3.length();
// Don't go any closer if a close proximity has been reached.
if (distance < 1) { return; }
// Scale the direction vector's magnitude down to match the speed.
var factor = this.data.speed / distance;
['x', 'y', 'z'].forEach(function (axis) {
directionVec3[axis] *= factor * (timeDelta / 1000);
});
// Translate the entity in the direction towards the target.
this.el.setAttribute('position', {
x: currentPosition.x + directionVec3.x,
y: currentPosition.y + directionVec3.y,
z: currentPosition.z + directionVec3.z
});
}
});
如何通过生态系统中的组件进行学习?
生态系统中有大量组件,其中大部分在 GitHub 上开源。一种学习方法是浏览其他组件的源代码,了解它们是如何构建的以及它们提供的用例。以下是一些值得一看的地方:
-
A-Frame core components - A-Frame 标准组件的源代码。
-
A-Painter components - A-Painter 的应用程序特定组件。
-
A Week of A-Frame Weekly Series
-
Official Site
-
Community
-
Components on npm
-
Twitter
如何发布组件?
实践中的许多组件都是特定于应用程序的组件或一次性组件。但是,如果您编写的组件可能对社区有用并且足够通用,可以在其他应用程序中工作,那么您应该发布它!
对于组件模板,我们建议使用 angle
。 angle
是 A-Frame 的命令行界面;其功能之一是设置组件模板以发布到 GitHub 和 npm,并与生态系统中的所有其他组件保持一致。要安装模板:
npm install -g angle && angle initcomponent
initcomponent
将询问一些信息,例如组件名称以设置模板。编写一些代码、示例和文档,然后发布到 GitHub 和 npm!