【开发指南】HTML和JS编写多用户VR应用程序的框架

1.概述

Networked-Aframe 的工作原理是将实体及其组件同步到连接的用户。要连接到房间,您需要将networked-scene组件添加到a-scene元素。对于要同步的实体,请向其添加networked组件。默认情况下,positionrotation组件是同步的,但如果您想同步其他组件或子组件,则需要定义架构。有关网络消息的更高级控制,请参阅广播自定义消息和选项部分。

2.场景组件

A-Frame<a-scene>上的组件。

复制代码
<a-scene networked-scene="
  serverURL: /;
  app: <appId>;
  room: <roomName>;
  connectOnLoad: true;
  onConnect: onConnect;
  adapter: wseasyrtc;
  audio: false;
  video: false;
  debug: false;
">
  ...
</a-scene>

|---------------|------------------------------------------------------------------|-----------|
| 属性 | 描述 | 默认值 |
| serverURL | 选择 WebSocket/信令服务器所在的位置。 | / |
| app | 唯一的应用程序名称。不允许有空格。 | default |
| room | 独特的房间名称。每个应用程序可以有多个。不允许有空格。每个应用程序可以有多个房间,客户端只能连接到同一应用程序和房间中的客户端。 | default |
| connectOnLoad | 网页加载后立即连接到服务器。 | true |
| onConnect | 当客户端成功连接到服务器时调用的函数。 | onConnect |
| adapter | 您要使用的网络服务,请参阅适配器。 | wseasyrtc |
| audio | 打开/关闭您的应用程序的麦克风音频流。仅当所选适配器支持时才有效。 | false |
| video | 打开/关闭您的应用程序的视频流。仅当所选适配器支持时才有效。 | false |
| debug | 打开/关闭 Networked-Aframe 调试日志。 | false |

3.连接

默认情况下,networked-scene将自动连接到您的服务器。为了防止这种情况发生并控制何时连接,请在networked-scene中将connectOnLoad设置为 false。当您准备好连接时,会在a-scene元素上发出connect事件。

复制代码
AFRAME.scenes[0].emit('connect');

4.断开连接

要断开连接,只需从a-scene元素中删除networked-scene组件即可。

复制代码
AFRAME.scenes[0].removeAttribute('networked-scene');

从页面中完全删除a-scene也可以彻底断开连接。

5.创建网络实体

复制代码
<a-assets>
  <template id="my-template">
    <a-entity>
      <a-sphere color="#f00"></a-sphere>
    </a-entity>
  </template>
</a-assets>

<!-- Attach local template by default -->
<a-entity networked="template: #my-template">
</a-entity>

<!-- Do not attach local template -->
<a-entity networked="template:#my-template;attachTemplateToLocal:false">
</a-entity>

创建要在客户端之间同步的模板实例。默认情况下,位置和旋转将同步。buffered-interpolation库用于允许更少的网络更新,同时保持平滑的运动。

模板只能有一个根元素。当attachTemplateToLocal设置为true时,该元素上的属性将被复制到本地实体,并且子元素将被附加到本地实体。远程实例化的实体将是模板根元素的副本,并添加了networked组件。

(1)attachTemplateToLocal=true示例

复制代码
<a-entity wasd-controls networked="template:#my-template">
</a-entity>

<!-- Locally instantiated as: -->
<a-entity wasd-controls networked="template:#my-template">
  <a-sphere color="#f00"></a-sphere>
</a-entity>

<!-- Remotely instantiated as: -->
<a-entity networked="template:#my-template;networkId:123;">
  <a-sphere color="#f00"></a-sphere>
</a-entity>

(2)attachTemplateToLocal=false示例

复制代码
<a-entity wasd-controls networked="template:#my-template;attachTemplateToLocal:false;">
</a-entity>

<!-- No changes to local entity on instantiation -->

<!-- Remotely instantiated as: -->
<a-entity networked="template:#my-template;networkId:123;">
  <a-sphere color="#f00"></a-sphere>
</a-entity>
复制代码

|-----------------------|---------------------------------------------|-------|
| 属性 | 描述 | 默认值 |
| template | 存储在<a-assets>中的模板标签的 css 选择器 | ''" |
| attachTemplateToLocal | 设置为 false 时,不附加本地用户的模板。当本地和远程存在不同行为时,这非常有用。 | true |
| persistent | 在远程创建者(而非所有者)断开连接时,尝试获取持久实体的所有权而不是删除它们 | false |

6.删除网络实体

目前只有网络实体的创建者可以删除它。要删除,只需使用常规 DOM API 从 HTML 中删除元素,Networked-Aframe 将自动处理同步。

7.同步自定义组件

默认情况下,根实体上的positionrotation组件是同步的。

要同步其他组件和子实体的组件,您需要为每个模板定义一个架构。以下是定义和添加架构的方法:

复制代码
NAF.schemas.add({
  template: '#avatar-template',
  components: [
    'position',
    'rotation',
    'scale',
    {
      selector: '.hairs',
      component: 'show-child'
    },
    {
      selector: '.head',
      component: 'material',
      property: 'color'
    },
  ]
});

根实体的组件可以用组件的名称来定义。子实体的组件可以使用具有selector字段和component字段的对象来定义,该字段使用document.querySelector使用的标准 CSS 选择器它指定组件的名称。要仅同步多属性组件的一个属性,请添加带有属性名称的property字段。

定义架构后,通过调用NAF.schemas.add(YOUR_SCHEMA)将其添加到架构列表中。

组件数据由 A-Frame 组件data属性检索。在网络更新期间,每个组件的数据都会根据其之前的同步值进行检查;如果数据对象发生了任何变化,它将通过网络同步。

8.同步组件优化

对于每个组件,您可以定义一个requiresNetworkUpdate函数,该函数采用当前值,如果当前值较之前值发生更改,则返回 true。如果当前值和先前值足够接近,您可以返回 false,以免将此更改发送给其他参与者。

默认情况下,当您未定义它时,它始终使用defaultRequiresUpdate函数(在networked.js顶部定义),该函数使用通用deepEqual函数来将当前值与前一个值进行比较,当两个值不同时使用cachedData = AFRAME.utils.clone(newData);以保留前一个值以供下次比较。AFRAME.utils.clone实现正做JSON.parse(JSON.stringify(obj)),可以与任何类型一起使用,但这可能不是 Vector3 类型(如位置、旋转、缩放)的最佳性能实现。

只是为了让你知道这是在做什么:

复制代码
> const v = new THREE.Vector3(1,2,3);
Vector3 {x: 1, y: 2, z: 3}
> JSON.parse(JSON.stringify(v))
{x: 1, y: 2, z: 3}

因此,如果值发生变化,每次同步过程完成时都会在内存中创建一个新对象,这些对象会在某一时刻被垃圾收集。如果场景很重,垃圾收集通常可能需要 1 毫秒或更长的时间,结果可能是浏览器丢掉一些帧,因此没有一致的 fps。您可以使用 Chrome 分析器来确认这一点。对于移动的头像来说,这一点几乎不会被注意到,但如果用户拥有大量连续移动的对象,这对于您的用例可能很重要。

此外,使用当前的 aframewasd-controls实现以及位置平滑方式,在用户停止按键后 2 秒,玩家位置的变化仍然低于毫米精度,因此通过网络发送大量 NAF 消息视觉上不易察觉的位置变化。 NAF 已经包含位置插值来平滑接收到的位置变化,因此通过网络发送所有这些位置变化甚至是多余的。

您可以使用专用函数来比较给定精度的两个 Vector3,通过避免创建新对象,通过网络和内存发送更少的消息来实现更好的性能:

复制代码
const vectorRequiresUpdate = epsilon => {
  return () => {
    let prev = null;

    return curr => {
      if (prev === null) {
        prev = new THREE.Vector3(curr.x, curr.y, curr.z);
        return true;
      } else if (!NAF.utils.almostEqualVec3(prev, curr, epsilon)) {
        prev.copy(curr);
        return true;
      }

      return false;
    };
  };
};

这个函数实际上是在NAF.utils.vectorRequiresUpdate中定义的,供你使用。

要在网络模式中使用它以获得 1 毫米的位置精度和 0.5 度的旋转精度,请按如下方式使用:

复制代码
{
  template: '#avatar-template',
  components: [
    {
      component: 'position',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.001)
    },
    {
      component: 'rotation',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.5)
    }
  ]
}

从0.11.0版本开始,同步位置和旋转的默认模式使用上述优化。

9.同步嵌套模板 - 例如:手

要同步嵌套模板,请像这样设置 HTML 节点:

复制代码
<a-entity id="player" networked="template:#player-template;attachTemplateToLocal:false;" wasd-controls>
  <a-entity camera look-controls networked="template:#head-template;attachTemplateToLocal:false;"></a-entity>
  <a-entity hand-controls="hand:left" networked="template:#left-hand-template"></a-entity>
  <a-entity hand-controls="hand:right" networked="template:#right-hand-template"></a-entity>
</a-entity>

在此示例中,头部/摄像头、控制器的左手和右手将生成自己的模板,这些模板将独立于根玩家进行联网。注意:这与当前不支持的手部追踪无关。这种父子关系仅在一个级别之间有效,即。子实体的直接父实体必须具有networked组件。

您需要自己定义左手和右手模板,以便向其他用户显示手部模型。只有位置和旋转会同步给其他用户。要同步手势,请参阅下面的networked-hand-controls组件。

10.带同步手势的跟踪控制器

这是比上述更简单的替代方案。 NAF 允许轻松添加其他人可见的手部模型,这些模型显示与触摸的按钮相匹配的模拟手势(不是手部跟踪),这样您就可以向房间里的其他人指向并竖起大拇指或握拳。

您所要做的就是使用内置的networked-hand-controls组件,将这两个实体添加为相机装备的子级:

复制代码
<a-entity
  id="my-tracked-left-hand"
  networked-hand-controls="hand:left"
  networked="template:#left-hand-default-template"
></a-entity>
<a-entity
  id="my-tracked-right-hand"
  networked-hand-controls="hand:right"
  networked="template:#right-hand-default-template"
></a-entity

您可以设置的公共架构属性有:

|--------------------|-----------------|----------|-------------------------------------|
| 属性 | 描述 | 默认值 | 取值范围 |
| color | 将被设置为材质颜色 | white | |
| hand | 指定实体是用于左手还是右手 | left | left, right |
| handModelStyle | A-Frame中可用的内置模型 | highPoly | highPoly, lowPoly, toon, controller |
| customHandModelURL | 可选的自定义手模型网址 | | |

请注意"控制器"选项------它将使用控制器本身的模型,根据您的平台自动正确设置------它还将广播模型支持的按钮网格更新。 (不幸的是,Quest 2 模型按钮网格目前存在一个错误,因此不会显示任何更新。)

networked-hand-controls正在完全替换hand-controls,不要同时使用两者。如果您使用如上所述的网络组件,则无需为每只手定义模板和网络架构。默认模板和网络模式已定义如下:

复制代码
<template id="left-hand-default-template">
  <a-entity networked-hand-controls="hand:left"></a-entity>
</template>
<template id="right-hand-default-template">
  <a-entity networked-hand-controls="hand:right"></a-entity>
</template>

NAF.schemas.add({
  template: '#left-hand-default-template',
  components: [
    {
      component: 'position',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.001)
    },
    {
      component: 'rotation',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.5)
    },
    'networked-hand-controls'
  ]
});
NAF.schemas.add({
  template: '#right-hand-default-template',
  components: [
    {
      component: 'position',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.001)
    },
    {
      component: 'rotation',
      requiresNetworkUpdate: NAF.utils.vectorRequiresUpdate(0.5)
    },
    'networked-hand-controls'
  ]
});

11.发送自定义消息

复制代码
NAF.connection.subscribeToDataChannel(dataType, callback)
NAF.connection.unsubscribeToDataChannel(dataType)

NAF.connection.broadcastData(dataType, data)
NAF.connection.broadcastDataGuaranteed(dataType, data)

NAF.connection.sendData(clientId, dataType, data)
NAF.connection.sendDataGuaranteed(clientId, dataType, data)

订阅和取消订阅dataType指定的网络消息的回调。使用broadcastData功能向房间内的所有客户端广播数据。要仅发送到特定客户端,请改用sendData函数。

|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数 | 描述 |
| clientId | 将此数据发送到的 ClientId |
| dataType | 用于标识网络消息的字符串。u(更新)、um(UpdateMulti)和r(删除)是保留数据类型,请不要使用它们 |
| callback | 收到类型dataType的消息时调用的函数。参数:function(senderId, dataType, data, targetObj)使用 easyrtc 适配器,targetObj在广播消息时可以是{targetRoom: 'roomId'},或者在向特定参与者发送消息时可以是{targetEasyrtcid: 'targetId'}。对于 janus 适配器,senderId 始终为 null,并且targetObj更多的是source参数,通常等于"janus-event"。 |
| data | 要发送给所有其他客户端的对象 |

12.转让实体所有权

实体的所有者负责同步其组件数据。当用户想要修改另一个用户的实体时,他们必须首先获得该实体的所有权。所有权转移示例和切换所有权组件展示了如何获取实体的所有权并更新它。

复制代码
NAF.utils.takeOwnership(entityEl)

取得实体的所有权。

复制代码
NAF.utils.isMine(entityEl)

检查您是否拥有指定实体。

13.事件

当 NAF 中发生某些事情时,事件就会被触发。要订阅这些事件,请遵循以下模式:

复制代码
document.body.addEventListener('clientConnected', function (evt) {
  console.error('clientConnected event. clientId =', evt.detail.clientId);
});

创建document.body元素后需要订阅事件。这可以通过等待document.body``onLoad方法或使用 NAF 的onConnect函数来实现。使用 NAF 活动演示作为示例。

事件列表:

|--------------------|------------------|------------------------------------------|
| 事件 | 描述 | 取值范围 |
| clientConnected | 当另一个客户端连接到您时触发 | evt.detail.clientId- 连接客户端的ClientId |
| clientDisconnected | 当另一个客户端与您断开连接时触发 | evt.detail.clientId- 断开连接的客户端的ClientId |
| entityCreated | 创建网络实体时触发 | evt.detail.el- 新实体 |
| entityRemoved | 删除网络实体时触发 | evt.detail.networkId- 已删除实体的网络ID |

以下事件在networked组件上触发。有关示例,请参阅切换所有权组件。

所有权转让事件列表:

|-------------------|-----------------|----------------------------------------|
| 事件 | 描述 | 参数范围 |
| ownership-gained | 当网络实体的所有权被夺取时触发 | evt.detail.el- 获得所有权的实体 |
| | | evt.detail.oldOwner- 前任所有者的 clientId |
| ownership-lost | 当网络实体的所有权丢失时触发 | evt.detail.el- 失去所有权的实体 |
| | | evt.detail.newOwner- 新所有者的 clientId |
| ownership-changed | 当网络实体的所有权更改时触发 | evt.detail.el- 失去所有权的实体 |
| | | evt.detail.oldOwner- 前任所有者的 clientId |
| | | evt.detail.newOwner- 新所有者的 clientId |

14.适配器

NAF 可与多个网络库和服务一起使用。适配器是一个向 NAF 添加对库的支持的类。如果您只是在开发一个小项目或概念验证,那么您可能会使用默认配置,并且可以跳过本节。评估不同适配器时应考虑的因素包括:

  • 一间房间需要支持多少个并发用户?

  • 您想托管自己的服务器吗?或者像 Firebase 这样的"无服务器"解决方案可以完成这项工作吗?

  • 您需要音频(麦克风)流吗?

  • 您需要自定义服务器端逻辑吗?

  • 您想要 WebSocket(客户端-服务器)网络架构还是 WebRTC(点对点)网络架构?

默认情况下使用wseasyrtc适配器,它不支持音频并使用 TCP 连接。这对于生产部署来说并不理想,但是由于 WebRTC 固有的连接问题,我们将其设置为默认值。要通过 WebRTC 支持音频,请确保服务器使用 https 并将适配器更改为easyrtc(这使用 UDP)。

支持的适配器列表:

|-----------|---------------------------------------|----------------|----------------------|-------------------------|
| 适配器 | 描述 | 音频/视频支持情况 | 基于WebSocket 或 WebRTC | 如何启动 |
| wseasyrtc | 默认 - 使用 open-easyrtc 库 | No | WebSocket | npm run dev |
| easyrtc | 使用 open-easyrtc 库 | 音频和视频(相机和屏幕共享) | WebRTC | npm run dev |
| janus | 使用 Janus WebRTC 服务器和 janus-plugin-sfu | 音频和视频(相机或屏幕共享) | WebRTC | 参阅 naf-janus-adapter |
| socketio | 无需外部库的 SocketIO 实现(服务器支持房间实例化) | No | WebSocket | npm run dev-socketio |
| webrtc | 无需外部库的原生 WebRTC 实现(正在进行中,目前没有维护者) | 音频 | WebRTC | npm run dev-socketio |
| Firebase | 用于 WebRTC 信号传输的 Firebase(目前没有维护者) | No | WebRTC | 参阅 naf-firebase-adapter |
| uWS | uWebSockets 的实现(目前没有维护者) | No | WebSocket | 参阅 naf-uws-adapter |

表中的 WebRTC 表示组件更新使用 WebRTC 数据通道 (UDP),而不是 WebSocket (TCP)。您仍然有一个用于信令部分的 WebSocket。

更为详细比较,请参阅文档 NAF 适配器比较

15.音频

audio: true添加到networked-scene组件(并使用支持它的适配器)后,默认情况下您将听不到任何音频。尽管音频将进行流式传输,但在创建具有networked-audio-source的实体之前,它是听不到的。来自该实体所有者的音频将从该实体的位置在 3D 空间中发出。networked-audio-source组件必须与networked组件一起添加到实体(或实体的子实体)。

要使麦克风静音/取消静音,您可以使用以下 API(easyrtc 和 janus 适配器):

复制代码
NAF.connection.adapter.enableMicrophone(enabled)

其中enabledtruefalse

16.视频

video: true(janus 适配器不需要)添加到networked-scene组件(并使用支持它的适配器)后,默认情况下您将看不到任何视频。尽管视频将进行流式传输,但在创建使用带有networked-video-source的网格(例如<a-plane>)的实体之前,它是不可见的。来自该实体所有者的视频将在 3D 空间中从该实体的位置可见。networked-video-source组件必须添加到具有networked组件的实体的<a-plane>子实体中。

目前,这仅适用于支持getMediaStream(clientId, type="video")API 的 easyrtc 和 janus 适配器。

请参阅,该示例显示了没有音频的用户摄像头。

要禁用/重新启用相机,您可以使用以下 API(仅限 easyrtc 适配器):

复制代码
NAF.connection.adapter.enableCamera(enabled)

其中enabledtruefalse

使用 easyrtc 适配器,您可以使用addLocalMediaStreamremoveLocalMediaStreamAPI 添加额外的视频轨道,例如屏幕共享:

复制代码
navigator.mediaDevices.getDisplayMedia().then((stream) => {
  NAF.connection.adapter.addLocalMediaStream(stream, "screen");
});

NAF.connection.adapter.removeLocalMediaStream("screen");

请参阅多流示例,该示例使用带有networked-video-source="streamName: screen"的第二个平面向其他参与者显示屏幕共享。请务必查看此示例 html 文件末尾的注释以了解已知问题。

17.杂项

复制代码
NAF.connection.isConnected()

如果已与信令服务器建立连接,则返回 true。

复制代码
NAF.connection.getConnectedClients()

返回当前连接的客户端列表。

18.选项

复制代码
NAF.options.updateRate

每秒调用网络组件sync函数的频率。对于大多数社交 VR 应用程序来说,10-20 是正常的。默认为15

复制代码
NAF.options.useLerp

默认情况下,当创建实体时,buffered-interpolation库用于平滑位置、旋转和缩放网络更新。如果您不希望在创建时使用此功能,请将其设置为 false。

19.离线使用

NAF 已经包含 easyrtc,因此运行npm run dev将提供完全有效的解决方案,而无需访问外部服务器。不过,这些示例确实依赖于 AFrame 和其他未与 NAF 打包的依赖项。因此,必须首先使 AFrame 适应离线工作,然后对所有其他组件执行相同的操作。这基本上可以归结为下载所使用的脚本及其内容,例如 3D 模型、字体等资产。建议在网络控制台打开时加载页面并识别哪些请求来自主机外部。

对于 VR,您还需要 https,因为浏览器需要它才能实现沉浸式模式。server/easyrtc-server.js文件中提供了说明。也就是说,您必须生成密钥和证书,将它们添加到本地 CA,然后通过 NAF 提供的 Express 服务器加载它们。确保在server/easyrtc-server.js顶部正确配置,并按照说明通过https.createServer进一步向下启用 https 本身。一旦您连接到 VR 中的 NAF 服务器,浏览器仍然会抱怨证书未知。您可以单击高级并继续。

相关推荐
Dovis(誓平步青云)4 小时前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
wgc2k4 小时前
Node.js游戏服务器项目移植-2: 用TypeScript还是Javascript
服务器·游戏·node.js
海兰11 小时前
【文字三国志:第一篇】天命重构,大语言模型(LLM)动态生成文言风格的叙事文本的文字游戏
人工智能·游戏·语言模型
isyangli_blog12 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb20081113 小时前
FastAPI APIRouter
开发语言·python
Benszen13 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆13 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木13 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充13 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~13 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言