A hands-on workshop on how to how to build a simple WebVR game application using A-Frame (an easy to use and open source virtual reality web framework). In the interest of time, this workshop will focus on setting up a basic interaction.
WebVR is a JavaScript API that allows VR experiences to run in a web browser on VR headsets, mobile phones and tablets, and regular computers.
Technically, A-Frame is an abstraction of three.js, a JavaScript library that uses the WebGL API to render 3D graphics in the browser.
A-Frame is a web framework for building virtual reality (VR) experiences for the web. At its simplest, It is an HTML web page that includes the A-Frame library, making it simple to get started.
First, create a new directory called aframe-webvr-game
.
$ mkdir aframe-webvr-game
Change into the directory and and create and index.html
file. Then, run git init
and npm init
(optional).
$ cd aframe-webvr-game
$ touch index.html
$ git init
$ npm init --yes
Note: The first command initializes an empty Git repository and the second walks you through creating a package.json file.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>A-Frame WebVR Gane</title>
<!-- aframe CDN -->
<script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
</head>
<body>
<!-- HTML goes here -->
</body>
</html>
In the index.html
file, start by creating a simple HTML document with basic head and body tags. Then, include the A-Frame CDN <script>
in the <head>
tag.
In A-Frame, scenes are enclosed in <a-scene></a-scene>
tags. It's what creates the stage for you to place 3D objects in, initializes the camera, the WebGL renderer and handles other boilerplate. It should be the outermost element wrapping everything else inside it.
Let's create an empty scene by adding an <a-scene>
element inside the <body>
element:
<body>
<a-scene></a-scene>
</body>
Every scene needs a background which can be a color or 360° image. In A-Frame, it’s represented by the <a-sky>
primitive. A background with a simple color looks like the following:
<body>
<a-scene>
<a-sky color="#A6CFE2"></a-sky>
</a-scene>
</body>
The floor is represented by the <a-plane>
primitive. You can give it a few attributes like color, width, and rotation. In order to make the plane parallel to the ground or make a plane the ground itself, it must be rotated around the X-axis.
<a-plane
rotation="-90 0 0"
color="#52430e"
height="20"
width="100">
</a-plane>
Background images should be equirectangular and can be found in abundance on Flickr:
<a-scene>
<a-assets>
<img id="city" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/city.jpg">
</a-assets>
<a-sky src="#city"></a-sky>
</a-scene>
Take note of A-Frame’s asset management system,
<a-assets>
used to preload images, videos, sound and 3D models.
There are two ways that we can populate the scene we’ve just created: 1) use primitive shapes; and 2) import 3D models.
Let’s jump to the AFRAME documentation for the sphere primitive. The following sphere has two attributes, color and radius; however, we can also add attributes like scale and position.
<a-sphere
color="yellow"
radius="5"
position="0 4 -10"
scale="0.25 0.25 0.25"></a-sphere>
We can also use an entity (I liken it to a div in HTML) and add attributes like geometry, material, and animation.
<a-entity
id='ball'
position="0.05 1 -5"
animation="property: position; to: 0.25 1.5 0.5; dur: 5000; easing: linear"
position="0 1 -4"
material="color:green;"
geometry="primitive:sphere; radius: 0.05;">
</a-entity>
For characters in the game, we are going to use a 3D model created in SketchFab. A-Frame uses the glTF format. We can load a glTF model by pointing to an asset that specifies the src for the file:
<a-scene>
<a-assets>
<a-asset-item id="model-name" src="/path/to/model-name.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#model-name"></a-entity>
</a-scene>
First, let’s download the model from Google Poly. Create an assets/models
folder and add the downloaded models into it. For this exercise, I've chosen avocado.
Preload the models using the <a-asset-item>
tag and then add it to the assets section of your code base:
<a-scene>
<a-assets>
<a-asset-item id="avocado" src="./assets/models/Avocado/Avocado.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#avocado"></a-entity>
</a-scene>
Outside the a-assets section, create several entities to load the avocado model:
<a-entity
class="item"
cursor-listener
gltf-model="#avocado"
position="0.05 1 -1"
scale="2 2 2">
</a-entity>
The additional attributes available on our entity -- position, which sets x, y, and z coordinates on each entity; scale, which sets blah; and the gltf-model, which loads the model into the frame -- allow us to customize the placement etc of each extity/avocado.
We can make the avocado respond to clicks by creating a custom component. The cursor component provides hover and click states for interaction.
Create a folder assets/components
and add a new file, cursor-listener.js
, with the following code:
AFRAME.registerComponent('cursor-listener', {
init: function () {
let el = this.el;
el.addEventListener('click', evt => {
if (el.parentNode.hasChildNodes()){
console.log('I was clicked', el);
el.parentNode.removeChild(el);
}
})
}
});
We're going to be using a JavaScript method called hasChildNodes(). It's a method that returns a Boolean value indicating whether the given Node has child nodes or not.
When you click on the parentNode, the removeChild() method removes the avocado from the DOM. We're setting up the functionality of the Component so that, when the cursor is placed over the avocado and clicked, it is removed form the DOM.
This feature casts a 'ray' between the origin (cursor) and the object you're trying to interact with for N distance. And every entity that falls along this 'ray' get removed from the DOM.
The cursor component depends on the raycaster and adds events/states to act like a cursor.
By default with autoRefresh set to true, the raycaster component will automatically refresh this list when it detects entities or components are added and removed from the DOM.
AFRAME.registerComponent('raycaster-autorefresh', {
init: function () {
const el = this.el;
this.el.addEventListener('model-loaded', function () {
console.log('the raycaster')
const cursorEl = el.querySelector('[raycaster]');
cursorEl.components.raycaster.refreshObjects();
});
}
});
A-Frame will call .refreshObjects()
automatically when an entity is appended or detached from the scene, but it will not get called during normal DOM mutations (e.g., some entity changes its class).
In order to use these two components, we have to include them in the head section of the index.html:
<script src="assets/components/cursor-listener.js"></script>
<script src="assets/components/raycaster-autorefresh.js"></script>
THen add it to the <a-scene>
tag:
<a-scene raycaster-autorefresh >
Add a camera to the scene with default origin at 0 1.6 0
in desktop mode and 0 0 0
in VR mode. This roughly corresponds to the average eye height of most humans.
<a-camera position="0 1.6 0">
<a-cursor></a-cursor>
</a-camera>
Notice the
<a-cursor>
inside of the camera. This will draw a little circular cursor, which is important for displays that don’t have controllers, such as Google cardboard.
We can animate an entity by placing the animation attribute on the entity you want to animate and passing it several properties -- duration, easing, and position.
<a-entity
...
position="0.05 1 -5"
animation="property: position; to: 0.25 1.5 0.5; dur: 5000; easing: linear">
</a-entity>
- Duration -- indicates how long (in milliseconds) each cycle of the animation is.
- Easing -- defines how you want the entity to move/or what pattern do you want your animation to follow. In this case the easing is “linear”, which makes the animation constant in its movement.
- Loop -- defines how many times the animation should repeat. If the value is true, the animation will repeat infinitely.
In this animation, we want the avocado to rotate at 360° in the y axis, so we define the to
attribute to that value.
<a-entity
...
rotation="0 0 0"
animation="property: rotation; to: 0 360 0; loop: true; dur: 10000">
</a-entity>
- Loop -- indicates how many times the animation should repeat. If the value is true, the animation will repeat infinitely.
- Property -- identifies the property to animate. It can be a component name, or a plain attribute.
To add sound to the scene, a good resource is freesound.org or from a CDN. When adding sound, like images you musy preload and cache the audio tag within the asset management system:
<audio id="crickets" src="https://cdn.aframe.io/basic-guide/audio/backgroundnoise.wav" autoplay preload></audio>
Then, add the sound component to one of the entities in the scene:
<a-entity sound="src: #crickets"></a-entity>