1.概述
Networked-Aframe 的工作原理是將實體及其組件同步到連接的用戶。要連接到房間,您需要將networked-scene
組件添加到a-scene
元素。對于要同步的實體,請向其添加networked
組件。默認情況下,position
和rotation
組件是同步的,但如果您想同步其他組件或子組件,則需要定義架構。有關網絡消息的更高級控制,請參閱廣播自定義消息和選項部分。
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 | 存儲在 | ''” |
attachTemplateToLocal | 設置為 false 時,不附加本地用戶的模板。當本地和遠程存在不同行為時,這非常有用。 | true |
persistent | 在遠程創建者(而非所有者)斷開連接時,嘗試獲取持久實體的所有權而不是刪除它們 | false |
6.刪除網絡實體
目前只有網絡實體的創建者可以刪除它。要刪除,只需使用常規 DOM API 從 HTML 中刪除元素,Networked-Aframe 將自動處理同步。
7.同步自定義組件
默認情況下,根實體上的position
和rotation
組件是同步的。
要同步其他組件和子實體的組件,您需要為每個模板定義一個架構。以下是定義和添加架構的方法:
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-entityid="my-tracked-left-hand"networked-hand-controls="hand:left"networked="template:#left-hand-default-template"
></a-entity>
<a-entityid="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 | 用于標識網絡消息的字符串。 |
callback | 收到類型 |
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 | 當另一個客戶端連接到您時觸發 |
|
clientDisconnected | 當另一個客戶端與您斷開連接時觸發 |
|
entityCreated | 創建網絡實體時觸發 |
|
entityRemoved | 刪除網絡實體時觸發 |
|
以下事件在networked
組件上觸發。有關示例,請參閱切換所有權組件。
所有權轉讓事件列表:
事件 | 描述 | 參數范圍 |
ownership-gained | 當網絡實體的所有權被奪取時觸發 |
|
| ||
ownership-lost | 當網絡實體的所有權丟失時觸發 |
|
| ||
ownership-changed | 當網絡實體的所有權更改時觸發 |
|
| ||
|
14.適配器
NAF 可與多個網絡庫和服務一起使用。適配器是一個向 NAF 添加對庫的支持的類。如果您只是在開發一個小項目或概念驗證,那么您可能會使用默認配置,并且可以跳過本節。評估不同適配器時應考慮的因素包括:
-
一間房間需要支持多少個并發用戶?
-
您想托管自己的服務器嗎?或者像 Firebase 這樣的“無服務器”解決方案可以完成這項工作嗎?
-
您需要音頻(麥克風)流嗎?
-
您需要自定義服務器端邏輯嗎?
-
您想要 WebSocket(客戶端-服務器)網絡架構還是 WebRTC(點對點)網絡架構?
默認情況下使用wseasyrtc
適配器,它不支持音頻并使用 TCP 連接。這對于生產部署來說并不理想,但是由于 WebRTC 固有的連接問題,我們將其設置為默認值。要通過 WebRTC 支持音頻,請確保服務器使用 https 并將適配器更改為easyrtc
(這使用 UDP)。
支持的適配器列表:
適配器 | 描述 | 音頻/視頻支持情況 | 基于WebSocket 或 WebRTC | 如何啟動 |
wseasyrtc | 默認 - 使用 open-easyrtc 庫 | No | WebSocket |
|
easyrtc | 使用 open-easyrtc 庫 | 音頻和視頻(相機和屏幕共享) | WebRTC |
|
janus | 使用 Janus WebRTC 服務器和 janus-plugin-sfu | 音頻和視頻(相機或屏幕共享) | WebRTC | 參閱 naf-janus-adapter |
socketio | 無需外部庫的 SocketIO 實現(服務器支持房間實例化) | No | WebSocket |
|
webrtc | 無需外部庫的原生 WebRTC 實現(正在進行中,目前沒有維護者) | 音頻 | WebRTC |
|
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)
其中enabled
是true
或false
。
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)
其中enabled
是true
或false
。
使用 easyrtc 適配器,您可以使用addLocalMediaStream
和removeLocalMediaStream
API 添加額外的視頻軌道,例如屏幕共享:
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 服務器,瀏覽器仍然會抱怨證書未知。您可以單擊高級并繼續。