一、基于HTML和Primitives的表达
1.HTML - 超文本标记语言
A-Frame 基于 HTML 和 DOM 之上,使用自定义元素的 polyfill。 HTML 是 Web 的构建块,提供了最易于访问的计算语言之一。无需安装或构建步骤,使用 HTML 创建仅涉及 HTML 文件中的文本并在浏览器中打开 HTML 文件。由于大多数 Web 都是基于 HTML 构建的,因此大多数现有工具和库都可以使用 A-Frame,包括 React、Vue.js、Angular、d3.js 和 jQuery。
如果您没有太多 HTML 经验,没问题!它相当容易上手,甚至比 2D HTML 更容易掌握。一旦您掌握了 HTML 的一般结构或语法(开始标记、属性、结束标记),那么您就可以开始了!。
2.Primitives - 原语
虽然 HTML 层看起来很基础,但 HTML 和 DOM 只是 A-Frame 最外层的抽象层。在底层,A-Frame 是一个以声明方式公开的 Three.js 实体组件框架。
(1)什么是原语?
A-Frame 提供了一些称为原语的元素,例如 <a-box>
或 <a-sky>
,它们包装了实体-组件模式,以使其对初学者有吸引力。 A-Frame 提供的开箱即用的每个原语。开发人员也可以创建自己的原语。拿A-Frame整体介绍中的例子:
<html><head><script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script></head><body><a-scene><a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box><a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere><a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder><a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane><a-sky color="#ECECEC"></a-sky></a-scene></body>
</html>
原语主要充当新手的便利层(即语法糖)。现在请记住,原语的底层是 <a-entity>
:
-
有一个语义名称(例如
<a-box>
) -
拥有一组带有默认值的预设组件
-
将 HTML 属性映射或代理到组件数据
原语类似于 Unity 中的预置体。一些有关实体-组件-系统模式的文档将它们称为组合。他们将核心实体组件 API 抽象为:
-
将有用的组件与规定的默认值一起预先组合
-
充当复杂但常见的实体类型的简写(例如
<a-sky>
) -
为初学者提供熟悉的界面,因为 A-Frame 将 HTML 带到了新的方向
在底层,这个 <a-box>
原语:
<a-box color="red" width="3"></a-box>
表示这个实体组件形式:
<a-entity geometry="primitive: box; width: 3" material="color: red"></a-entity>
<a-box>
将 geometry.primitive
属性默认为 box
。该基元将 HTML width
属性映射到底层 geometry.width
属性,并将 HTML color
属性映射到底层 material.color
属性。
(2)如何将组件附加到原语?
原语只是底层的 <a-entity>
。这意味着原语具有与实体相同的 API,例如位置、旋转、缩放和附加组件。
例子:
让我们将社区物理组件附加到原语上。我们包含 Don McCurdy 的 aframe-physics-system
的源代码,并通过 HTML 属性附加物理组件:
<html><head><script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-physics-system@1.6.0/dist/aframe-physics-system.min.js"></script></head><body><a-scene physics><a-box position="-1 4 -3" rotation="0 45 0" color="#4CC3D9" dynamic-body></a-box><a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" static-body></a-plane><a-sky color="#ECECEC"></a-sky></a-scene></body>
</html>
(3)如何注册原语?
我们可以使用 AFRAME.registerPrimitive(name, definition)
注册我们自己的原语(即注册一个元素)。 name
是一个字符串,必须包含破折号(即 'a-foo'
)。 definition
是定义以下属性的 JavaScript 对象:
属性 | 描述 | 举例 |
defaultComponents | 指定原语默认组件的对象。键是组件的名称,值是组件的默认数据。 |
|
mappings | 指定 HTML 属性名称和组件属性名称之间映射的对象。每当更新 HTML 属性名称时,原语就会更新相应的组件属性。组件属性使用点语法 |
|
例子:
例如,下面是 A-Frame 对 <a-box>
原语的注册:
var extendDeep = AFRAME.utils.extendDeep;// The mesh mixin provides common material properties for creating mesh-based primitives.
// This makes the material component a default component and maps all the base material properties.
var meshMixin = AFRAME.primitives.getMeshMixin();AFRAME.registerPrimitive('a-box', extendDeep({}, meshMixin, {// Preset default components. These components and component properties will be attached to the entity out-of-the-box.defaultComponents: {geometry: {primitive: 'box'}},// Defined mappings from HTML attributes to component properties (using dots as delimiters).// If we set `depth="5"` in HTML, then the primitive will automatically set `geometry="depth: 5"`.mappings: {depth: 'geometry.depth',height: 'geometry.height',width: 'geometry.width'}
}));
例如,Don McCurdy 的 aframe-extras
包包含一个包装其 ocean
组件的 <a-ocean>
原语。这是 <a-ocean>
的定义。
AFRAME.registerPrimitive('a-ocean', {// Attaches the `ocean` component by default.// Defaults the ocean to be parallel to the ground.defaultComponents: {ocean: {},rotation: {x: -90, y: 0, z: 0}},// Maps HTML attributes to the `ocean` component's properties.mappings: {width: 'ocean.width',depth: 'ocean.depth',density: 'ocean.density',color: 'ocean.color',opacity: 'ocean.opacity'}
});
注册了 <a-ocean>
原语后,我们就可以使用一行传统的 HTML 创建海洋:
<a-ocean color="aqua" depth="100" width="100"></a-ocean>
二、基于ECS(实体-组件-系统)架构
1.什么是ECS?
ECS 架构是 3D 和游戏开发中常见且理想的模式,遵循组合优于继承和层次结构原则。
(1)ECS优势
-
通过混合和匹配可重用部件来定义对象时具有更大的灵活性。
-
消除了具有复杂交织功能的长继承链的问题。
-
通过解耦、封装、模块化、可重用性促进清洁设计。
-
就复杂性而言,构建 VR 应用程序的最具可扩展性的方法。
-
经过验证的 3D 和 VR 开发架构。
-
允许扩展新功能(可能将它们作为社区组件共享)。
在 2D Web 上,我们在层次结构中布置具有固定行为的元素。 3D和VR是不同的;有无限种可能的对象具有无限的行为。 ECS 提供了一种可管理的模式来构造对象类型。
以下是 ECS 架构的精彩介绍材料。我们建议浏览一下它们以更好地掌握其好处。 ECS 非常适合 VR 开发,而 A-Frame 完全基于此范例:
-
Entity-component-system on Wikipedia
-
What is an Entity System? by Adam Martin
-
Decoupling Patterns — Component on Game Programming Patterns
-
Evolve Your Hierarchy by Mick West
Unity是实现ECS的著名游戏引擎。尽管跨实体通信存在一些痛点,但我们将看到 A-Frame、DOM 和声明性 HTML 如何真正让 ECS 发光发热。
(2)ECS定义
ECS 的基本定义包括:
-
实体:是可以附加组件的容器对象。实体是场景中所有对象的基础。如果没有组件,实体既不会执行也不会渲染任何内容,类似于空的
<div>
。 -
组件:是可重用的模块或数据容器,可以附加到实体以提供外观、行为和/或功能。组件就像对象的即插即用。所有逻辑都是通过组件来实现的,我们通过混合、匹配和配置组件来定义不同类型的对象。就像炼金术一样!
-
系统:为组件类别提供全局范围、管理和服务。系统通常是可选的,但我们可以用它们来分离逻辑和数据;系统处理逻辑,组件充当数据容器。
(3)举例
通过将不同组件组合在一起构建的不同类型实体的一些抽象示例:
-
Box = Position + Geometry + Material
-
Light Bulb = Position + Light + Geometry + Material + Shadow
-
Sign = Position + Geometry + Material + Text
-
VR Controller = Position + Rotation + Input + Model + Grab + Gestures
-
Ball = Position + Velocity + Physics + Geometry + Material
-
Player = Position + Camera + Input + Avatar + Identity
作为另一个抽象示例,假设我们想要通过组装组件来构建汽车实体:
-
我们可以附加一个
material
组件,该组件具有影响汽车外观的“颜色”或“光泽度”等属性。 -
我们可以附加一个
engine
组件,该组件具有影响汽车功能的“马力”或“重量”等属性。 -
我们可以附加一个
tire
组件,该组件具有影响汽车行为的“轮胎数量”或“转向角”等属性。
因此,我们可以通过改变 material
、 engine
和 tire
组件的属性来创建不同类型的汽车。 material
、 engine
和 tire
组件不必相互了解,甚至可以在其他情况下单独使用。我们可以混合搭配它们来创造不同类型的车辆:
-
要创建船实体:删除
tire
组件。 -
创建摩托车实体:将
tire
组件的轮胎数量更改为2个,将engine
组件配置得更小。 -
要创建飞机实体:附加
wing
和jet
组件。
与传统的继承相比,如果我们想要扩展一个对象,我们必须创建一个试图处理所有内容或继承链的大类。
2.A-Frame中的ECS
A-Frame 具有代表每个 ECS 的 API:
-
实体:由
<a-entity>
元素和原型表示。 -
组件:由
<a-entity>
上的 HTML 属性表示。下面,组件是包含架构、生命周期处理程序和方法的对象。组件通过AFRAME.registerComponent (name, definition)
API 注册。 -
系统:由
<a-scene>
的 HTML 属性表示。系统在定义上与组件类似。系统通过AFRAME.registerSystem (name, definition)
API 注册。
(1)语法
创建<a-entity>
并将组件附加为 HTML 属性。大多数组件都有多个属性,这些属性由类似于HTMLElement.style
CSS 的语法表示。此语法采用以下形式:用冒号 (:
) 分隔属性名称和属性值,并用分号 (;
) 分隔不同的属性声明:
<a-entity ${componentName}="${propertyName1}: ${propertyValue1}; ${propertyName2}: ${propertyValue2}">
例如,我们有<a-entity>
并附加具有各种属性和属性值的几何体、材质、灯光和位置组件:
<a-entitygeometry="primitive: sphere; radius: 1.5"light="type: point; color: white; intensity: 2"material="color: white; shader: flat; src: glow.jpg"position="0 0 -5">
</a-entity>
(2)组合实体
我们可以附加更多组件来添加额外的外观、行为或功能(例如物理)。或者我们可以更新组件值来配置实体(以声明方式或通过.setAttribute
)。
由多个组件组成的一种常见实体类型是 VR 中玩家的手。玩家的手可以有很多组成部分:外观、手势、行为、与其他对象的交互性。
我们将组件插入手实体以提供其行为,就像我们为 VR 附加超能力或增强功能一样!下面的每个组件彼此不了解,但可以组合起来定义一个复杂的实体:
<a-entitytracked-controls <!-- Hook into the Gamepad API for pose. -->vive-controls <!-- Vive button mappings. -->oculus-touch-controls <!-- Oculus button mappings. -->hand-controls <!-- Appearance (model), gestures, and events. -->laser-controls <!-- Laser to interact with menus and UI. -->sphere-collider <!-- Listen when hand is in contact with an object. -->grab <!-- Provide ability to grab objects. -->throw <!-- Provide ability to throw objects. -->event-set="_event: grabstart; visible: false" <!-- Hide hand when grabbing object. -->event-set="_event: grabend; visible: true" <!-- Show hand when no longer grabbing object. -->
>
(3)基于声明式 DOM 的 ECS
A-Frame 通过使其具有声明性并基于 DOM,将 ECS 提升到了另一个水平。传统上,基于 ECS 的引擎将通过代码创建实体、附加组件、更新组件、删除组件。但 A-Frame 具有 HTML 和 DOM,这使得 ECS 符合人体工程学并解决了它的许多弱点。以下是 DOM 为 ECS 提供的功能:
-
使用查询选择器引用其他实体:DOM 提供了强大的查询选择器系统,它允许我们查询场景图并选择与条件匹配的一个或多个实体。我们可以通过 ID、类或数据属性获取对实体的引用。因为 A-Frame 基于 HTML,所以我们可以直接使用查询选择器。
document.querySelector('#player')
。 -
与事件解耦的跨实体通信:DOM 提供侦听和发出事件的能力。这提供了实体之间的发布-订阅通信系统。组件不必相互了解,它们只需发出一个事件(可能会冒泡),其他组件可以监听这些事件而无需相互调用。
ball.emit('collided')
。 -
使用 DOM API 进行生命周期管理的 API:DOM 提供 API 来更新 HTML 元素和树,包括
.setAttribute
、.removeAttribute
、.createElement
和.removeChild
-
使用属性选择器进行实体过滤:DOM 提供属性选择器,允许我们查询具有或不具有某些 HTML 属性的一个或多个实体。这意味着我们可以请求具有或不具有特定组件集的实体。
document.querySelector('[enemy]:not([alive])')
。 -
声明性:DOM 提供 HTML。 A-Frame 在 ECS 和 HTML 之间建立了桥梁,使本已干净的模式具有声明性、可读性和可复制粘贴性。
(4)可扩展性
A-Frame组件无所不能。开发人员可以获得无需许可的创新来创建组件来扩展任何功能。组件可以完全访问 JavaScript、 Three.js 和 Web API(例如 WebRTC、语音识别)。
稍后我们将详细介绍如何编写 A-Frame 组件。作为预览,基本组件的结构可能如下所示:
AFRAME.registerComponent('foo', {schema: {bar: {type: 'number'},baz: {type: 'string'}},init: function () {// Do something when component first attached.},update: function () {// Do something when component's data is updated.},remove: function () {// Do something when the component or its entity is detached.},tick: function (time, timeDelta) {// Do something on every scene tick or frame.}
});
声明式 ECS 使我们能够编写 JavaScript 模块并通过 HTML 对其进行抽象。注册组件后,我们可以通过 HTML 属性以声明方式将此代码模块插入到实体中。这种从代码到 HTML 的抽象使 ECS 变得强大且易于推理。 foo
是我们刚刚注册的组件名称,数据包含 bar
和 baz
属性:
<a-entity foo="bar: 5; baz: bazValue"></a-entity>
(4)基于组件的开发模式
为了构建 VR 应用程序,我们建议将所有应用程序代码放置在组件(和系统)中。理想的 A-Frame代码库纯粹由模块化、封装和解耦的组件组成。这些组件可以单独进行单元测试,也可以与其他组件一起进行单元测试。
当仅使用组件创建应用程序时,其代码库的所有部分都可以重用!组件可以共享给其他开发人员使用,或者我们可以在其他项目中重用它们。或者可以对组件进行分叉和修改以适应其他用例。
一个简单的 ECS 代码库的结构可能如下:
index.html
components/ball.jscollidable.jsgrabbable.jsenemy.jsscoreboard.jsthrowable.js
(5)高阶组件
组件可以在实体上设置其他组件,使它们成为抽象上的更高阶或更高级别的组件。
例如,光标组件设置并构建在光线投射器组件之上。或者手动控制组件设置并构建在 vive-controls 组件和 oculus-touch-controls 组件之上,而 oculus-touch-controls 组件又构建在 tracked-controls 组件之上。
3.社区组件生态
组件可以共享到A-Frame生态系统中供社区使用。 A-Frame ECS 的美妙之处在于可扩展性。经验丰富的开发人员可以开发物理系统或图形着色器组件,而新手开发人员只需放入 <script>
标记即可从 HTML 获取这些组件并在场景中使用它们。我们可以使用强大的已发布组件,而无需接触 JavaScript。
(1)哪里可以找到组件?
市面上有数百个组件。我们尽力让它们被发现。如果您开发了组件,请通过这些渠道提交分享!
1)npm 新项目管理
大多数 A-Frame 组件都发布在 npm 和 GitHub 上。我们可以使用 npm 的搜索来搜索 aframe-components
。 npm 让我们可以按质量、受欢迎程度和维护情况进行排序。这是查找更完整的组件列表的好地方。
2)GitHub 项目
许多 A-Frame 应用程序纯粹是由组件开发的,其中许多 A-Frame 应用程序都是在 GitHub 上开源的。他们的代码库将包含我们可以直接使用、引用或复制的组件。需要关注的项目包括:
-
BeatSaver Viewer
-
Super Says
-
A-Painter
-
A-Blast
3)A-Frame博客
A-Frame 博客档案包含组件发布或更新时的详细信息,并且是查找组件链接的好地方。
4)A-Frame Wiki
A-Frame Wiki 是一项有用的社区驱动计划,用于收集有关可用 A-Frame 组件的信息和提示。鼓励每个人参与。添加和编辑信息非常容易。
(2)使用社区组件
一旦找到想要使用的组件,我们就可以将该组件包含为 <script>
标记并从 HTML 中使用它。
例如,我们使用 IdeaSpaceVR 的粒子系统组件:
1)使用unpkg
首先,我们必须获取组件 JS 文件的 CDN 链接。组件的文档通常会有 CDN 链接或使用信息。但获取最新 CDN 链接的一种方法是使用 unpkg.com。
unpkg 是一个 CDN,它自动托管发布到 npm 的所有内容。 unpkg 可以解析语义版本控制并为我们提供我们想要的组件的版本。 URL 采用以下形式:
https://unpkg.com/<npm package name>@<version>/<path to file>
如果我们想要最新版本,我们可以排除 version
:
https://unpkg.com/<npm package name>/<path to file>
我们可以排除 path to file
来浏览组件包的目录,而不是输入构建的组件 JS 文件的路径。 JS 文件通常位于名为 dist/
或 build/
的文件夹中,并以 .min.js
结尾。
对于粒子系统组件,我们前往:
https://unpkg.com/aframe-particle-system-component/
请注意结尾斜杠 ( /
)。找到我们需要的文件,右键单击,然后点击“复制链接到地址”,将 CDN 链接复制到剪贴板。
2)包含组件 JS 文件
然后转到我们的 HTML。在 <head>
下,A-Frame JS <script>
标记之后, <a-scene>
之前,我们将包含带有 <script>
的 JS 文件标签。
对于粒子系统组件,我们之前(在撰写本文时)发现的 CDN 链接是:
https://unpkg.com/aframe-particle-system-component@1.0.9/dist/aframe-particle-system-component.min.js
现在我们可以将它包含到我们的 HTML 中:
<html><head><script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-particle-system-component@1.0.9/dist/aframe-particle-system-component.min.js"></script></head><body><a-scene></a-scene></body>
</html>
3)使用组件
请遵循该组件的文档,了解如何在实现中使用它。但一般来说,使用涉及将组件附加到实体并对其进行配置。对于粒子系统组件:
现在我们可以将它包含到我们的 HTML 中:
<html><head><script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script><script src="https://unpkg.com/aframe-particle-system-component@1.0.9/dist/aframe-particle-system-component.min.js"></script></head><body><a-scene><a-entity particle-system="preset: snow" position="0 0 -10"></a-entity></a-scene></body>
</html>
(3)例子
<html><head><title>Community Components Example</title><meta name="description" content="Community Components Example"><script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-particle-system-component@a5a8449/dist/aframe-particle-system-component.js"></script><script src="https://cdn.jsdelivr.net/npm/aframe-simple-sun-sky@^1.2.2/simple-sun-sky.js"></script><script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@d5f3f8/dist/aframe-extras.min.js"></script></head><body><a-scene> <a-entity id="rain" particle-system="preset: rain; color: #24CAFF; particleCount: 5000"></a-entity><a-entity id="sphere" geometry="primitive: sphere"material="color: #EFEFEF; shader: flat"position="0 0.15 -5"light="type: point; intensity: 5"animation="property: position; easing: easeInOutQuad; dir: alternate; dur: 1000; to: 0 -0.10 -5; loop: true"></a-entity><a-entity id="ocean" ocean="density: 20; width: 50; depth: 50; speed: 4"material="color: #9CE3F9; opacity: 0.75; metalness: 0; roughness: 1"rotation="-90 0 0"></a-entity><a-simple-sun-sky sun-position="1 0.4 0"></a-simple-sun-sky><a-entity id="light" light="type: ambient; color: #888"></a-entity></a-scene></body>
</html>