Multi-user experiences on the Internet have come a long way since the emergence of online chat and the rise of social networks. Advances in virtual reality are making the Web a more immersive and interactive environment. Neal Stephenson envisioned this set of shared, persistent, and interconnected 3D virtual spaces and called it the metaverse; today we are building it with HTML, JavaScript, and A-Frame.

Given the gentle learning curve of HTML, A-Frame is the perfect choice for those who want to create virtual spaces on the Web with a few lines of HTML and JavaScript. With the implementation of link-traversal by browsers, we are a step closer to teleporting from one experience to another without commuting in the real world. However, a collaboration model to facilitate information exchange between the inhabitants of the metaverse is still missing. WebRTC plays a key role in enabling this exchange.

Peer-To-Peer communications with WebRTC

WebRTC is a Web API, present in the majority of browsers (including Safari as of this September), that allows the interchange of information without intermediaries, in a peer-to-peer fashion. With WebRTC, it is possible to implement the necessary infrastructure for persisting the experiences integrating the metaverse.

The biggest complexity of WebRTC arises from session management, peer discovery and signaling, all necessary to identify the connections between browsers. Once peer identities have been established, the standard can provide end-to-end channels for sharing media and data.

The sharedspace component

To bring the power of WebRTC to A-Frame users, I’ve been developing the sharedspace component. The component provides a collaboration model where participants can join or leave a named space, share audio and state, and send JSON-serializable objects to other peers.

The component does not deal with the WebRTC API directly. Instead, it uses a modified version of webrtc-swarm as a wrapper library. I chose it for its simplicity, footprint, clarity of the source code, and ease of use.

You can experiment with some VR chat on Glitch or browse the project to learn more about the sharedspace component. The repository includes extensive documentation with explanations and examples of other common use cases.

The sharedspace component is not a general network solution for A-Frame and it does not come with a straightforward way of sharing entities among multiple instances of an application. However, the ability to send data means it’s possible to create new forms of collaboration built on top of this component.

For a more general network component, take a look at the networked-aframe component by Hayden Lee.

Compatibility

The sharedspace component requires a browser with WebRTC support which means any recent version of the most popular mobile and desktop browsers. Chrome also requires the page supporting WebRTC to be served on HTTPS unless you are serving from localhost .

How does it work?

This is the minimal HTML code you need to implement a shared space:

<!-- Inside head --> <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script> <script src="https://cdn.rawgit.com/delapuente/aframe-sharedspace-component/master/dist/aframe-sharedspace-component.min.js"></script> <script src="https://rawgit.com/feiss/aframe-environment-component/master/dist/aframe-environment-component.min.js"></script> <!-- Inside body --> <a-scene> <a-entity sharedspace avatars> <!-- Actually, this is not needed but convenient to have some reference points --> <a-entity environment="preset: forest"><a-entity> </a-entity> </a-scene> <template> <a-sphere radius="0.1" color=”#ff0000”></a-sphere> </template>

However, the code above will always connect to a room named room-101 and the avatar representations will be red spheres, which are not an effective representation of human participants. Let’s customize the settings to change this.

You can follow along with me as I customize the code using this same minimal setup on Glitch: remix the project.

Important: To test your progress, you will need at least two clients connected to the same room. While testing, you will notice the peers are connected because the camera resets and the app no longer allows you to move the avatar around. When this happens, look for a red ball: this is the other peer.

Randomly generated room names

The sharedspace component will try to connect to the room as soon as the A-Frame scene is ready. Once the component has connected, changing its properties has no effect. To prevent the component from connecting the server, set the hold property to true :

<a-entity sharedspace="hold: true" avatars></a-entity>

Prepare a script to modify the scene once it has finished loading. Add the following script tag just before the closing tag of body :

<script> var scene = document.querySelector('a-scene'); (function start() { if (!scene.hasLoaded) { scene.addEventListener('loaded', start); return; } // Now it’s safe to change the scene components... }()); </script>

Replace the comment with the following JavaScript, which will check the current URL to find a room to connect to. If no room is found, the app will generate a new room and replace the URL in the address bar to let the user invite their friends to it:

var prefix = window.location.host.split('.')[0] + '-'; var currentUrl = new URL(window.location); var roomName = currentUrl.search.substr(1); if (!roomName) { roomName = prefix + Date.now(); currentUrl.search = roomName; history.pushState({}, '', currentUrl.href); } var room = document.querySelector('[sharedspace]'); room.setAttribute('sharedspace', { room: roomName, hold: false });

The most important part is the last line where you set the name of the room and reset the hold property to false , allowing the component to connect.

Custom avatars

The avatars component is available when installing the sharedspace component. It manages the A-Frame scene to provide an avatar representation to each participant. By default, the avatars component will search for a template tag and use its content for instantiating the avatar.

Replace the content of the template tag with the following primitives:

<template> <a-entity> <a-sphere radius="0.1" color="#ffffff"></a-sphere> <a-sphere position="0.05 0.03 -0.08" radius="0.02" segments-width="8" segments-height="8" color="#000000"></a-sphere> <a-sphere position="-0.05 0.03 -0.08" radius="0.02" segments-width="8" segments-height="8" color="#000000"></a-sphere> <a-sphere class="themable" position="0 -0.07 -0.1" scale="1 1 0.5" segments-width="4" segments-height="4" radius="0.02" color="#11fd3e"></a-sphere> <a-cone class="themable" position="0.03 -0.07 -0.1" rotation="0 0 90" scale="1 1 0.5" segments-radial="8" segments-height="1" height="0.03" radius-bottom="0.03" color="#1cff3c"></a-cone> <a-cone class="themable" position="-0.03 -0.07 -0.1" rotation="0 0 -90" scale="1 1 0.5" segments-radial="8" segments-height="1" height="0.03" radius-bottom="0.03" color="#1cff3c"></a-cone> </a-entity> </template>

Fixing orientation

Not knowing where the camera is pointing when you connect can be annoying. Let’s fix that. When a participant joins the room, the avatars component instantiates the avatar template and emits an avataradded event on its entity. This allows dynamic configuration of the template.

Locate the line where you get the room element and add the following code right there, (before setting hold to false ):

room.addEventListener('avataradded', function onAdded(evt) { var avatar = evt.detail.avatar; if (!avatar.hasLoaded) { avatar.addEventListener('loaded', onAdded.bind(null, evt)); return; } var avatarY = avatar.getAttribute('position').y; avatar.object3D.lookAt(new THREE.Vector3(0, avatarY, 0)); var radToDeg = THREE.Math.radToDeg; var rotation = avatar.object3D.rotation; rotation.y += Math.PI; avatar.setAttribute('rotation', { x: radToDeg(rotation.x), y: radToDeg(rotation.y), z: radToDeg(rotation.z) }); });

Notice that avataradded does not guarantee that the avatar entity has loaded. You should wait for the avatar to completely load before it’s safe to alter other components. The code uses the underlying Three.js API to calculate the correct orientation of the avatar.

Positional audio

Using WebRTC to stream audio is so common that the sharedspace component, in collaboration with the avatars component, makes it very straightforward. Simply set the audio property to true :

<a-entity sharedspace="hold: true; audio: true" avatars>

The next time you load the experience, the browser will ask for permission to share your microphone.

If the participants grant permission, the positional audio for the A-Frame avatars will be automatically managed by the avatars component. Positional audio means the sound will be panned left or right according to the relative position of the listener (i.e., the camera). Wearing headphones or earbuds enhances this effect.

Sharing position

You may have noticed that the avatar representing the user has a special treatment. Because its avatar ‘carries’ the camera, when you look around your orientation is shared by the other participants. By default, avatars will add some specific components to the user’s avatar.

You can control which components should be applied to the user’s avatar using A-Frame mixins. Mixins are component containers, and entities can set the mixin attribute to a list of mixin ids to inherit their components.

Add an a-assets tag just after the a-scene tag with a mixin inside and set its id to users :

<a-assets> <a-mixin id="user" visible="false" look-controls wasd-controls share="position, rotation"></a-mixin> </a-assets>

The share component (also available after registering sharedspace ) indicates which components should be kept in sync among other peers.

By setting the property onmyself of sharedspace to the id of the mixin, you’re instructing avatars to add that mixin to the user’s avatar.

<a-entity sharedspace="hold: true; audio: true" avatars="onmyself: user">

Sending and receiving messages

The sharedspace component allows the user to send messages to other peers. You’ll use this feature to force a change into the preset environment when pressing the spacebar.

Locate the line where you added the listener to the avataradded event and insert the following code for managing the environment presets:

var presets = [ 'contact', 'egypt', 'checkerboard', 'forest', 'goaland', 'yavapai', 'goldmine', 'threetowers', 'poison', 'arches', 'tron', 'japan', 'dream', 'volcano', 'starry', 'osiris' ]; var environment = document.querySelector('[environment]'); function setEnvironment(preset) { environment.setAttribute('environment', { preset: preset }); } function getNextPreset() { var currentPreset = environment.getAttribute('environment').preset; var index = presets.indexOf(currentPreset); return presets[(index + 1) % presets.length]; } // Here comes the code to send and receive message….

Finally, replace the comment with the code for receiving and sending messages:

window.addEventListener('keydown', function (evt) { if (evt.keyCode === 32 /* spacebar */) { var preset = getNextPreset(); setEnvironment(preset); room.components.sharedspace.send('*', { type: 'environment', preset: preset }); } }); room.addEventListener('participantmessage', function (evt) { if (evt.detail.message.type === 'environment') { var preset = evt.detail.message.preset; setEnvironment(preset); } });

What’s next?

This article shows how to evolve a minimal shared space setup into an appealing multi-user outdoor experience. But this is just the beginning. If you want to continue improving the demo, here are some things you can try:

The component repository includes an extensive explanation of the components API and a template VR Chat project on Glitch, including a source guide to get you familiarized with the components quickly.

Looking for more ideas? Here is another multi-user application powered by sharedspace in the Unbirthday Room.

Conclusion

Multi-user applications are not limited to chats: other participatory experiences fit the sharedspace model. Even if the proposed participation model is limited, other components can build on top of it to enable new interactions.