How to build a WebVR game with A-Frame

Adi Polak - Apr 11 '19 - - Dev Community

🐦 Follow me on Twitter, happy to take your suggestions on topics.

🕹️ Play the game
💻 Git repository

➡️ A few months ago, I received my first MR headset. As a geek, I got excited and started playing with it. It didn't take long before I felt like I needed to build something that involves writing code.

For years I did backend development and knew nothing about how frontend development works today. The memories I had from CSS consisted of 90% frustration and 10% relief that it was done.

However, one of my friends was also curious and we decided to investigate it.

We got together, made a good cup of coffee, got some cookies, set out our computers, and started reading. We decided to give A-Frame a try. A few hours went by, and we had a spinning gltf model and a game scene. Awesome! So much learning happened that day that we made a promise to share our findings with the community. We scheduled a meetup for Valentine's Day. However, we had zero experience in designing games. After thinking about it, we decided to keep it simple. We designed a game with one gesture, collecting hearts. The decision was final. We scheduled a live coding session. Where we show how every developer in the world can build a simple WebMR game. We will build a scene with spinning hearts, score, and a gesture of collecting hearts. For extra spice, this will be an infinite game, where for each heart collected, another heart will pop-up in a random location.

Wait a second, what is WebVR or WebMR?

Are you excited? Let's do this!

Prerequisites:

  1. Azure account
  2. Visual Studio code (VScode) - VS code
  3. VScode Azure storage extension
  4. npm

First things first. Let's create a project: Go to the desired directory or create one and run npm init. In bash it will be like this:

mkdir valentines_game
cd valentines_game
npm init -g
Enter fullscreen mode Exit fullscreen mode

The last command will ask for a project name, version, description and more. You don't have to answer it all and we can change it later. Npm creates a package.json with all the details provided.
In order to debug the game from the local machine, we will need to configure the server as well, so what you need to do is open the package.json file and update scripts to contain the follow:

 "scripts": {
    "start": "live-server web"
  }
Enter fullscreen mode Exit fullscreen mode

This will make sure that we can later use npm start and debug the game from local machine.

Next, run:

npm install
Enter fullscreen mode Exit fullscreen mode

Open VScode and create an html file named index.html. Create html and head tags. The head tag contains the metadata definition. Add a script tag which imports the aframe scripts for the project.

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>MR Valentines</title>
  <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
  <script src="https://rawgit.com/feiss/aframe-environment-component/master/dist/aframe-environment-component.min.js"></script>
</head>
</html>
Enter fullscreen mode Exit fullscreen mode

Let's run it, so we can see the updates live in the browser:

npm start
Enter fullscreen mode Exit fullscreen mode

Next step is creating an html body with scene tag. In AFrame as in games, the scene defines the window where we are located and what we see. a-entity is a tag for defining entities. At the moment, we use it to define our environment as you see below it is 'japan'.

<body>
  <a-scene>
    <a-entity environment="preset:japan"></a-entity>
  </a-scene>
</body>
Enter fullscreen mode Exit fullscreen mode

There are a few built-in environments. For example: egypt, checkerboard, forest, goaland, yavapai, goldmine arches, japan, dream, volcano, and more.

Next is the animated model: the heart. Download the Heart model.
Extract the zipped files. Put both bin and gltf files in the project directory. Next, add the heart tag:

 <a-entity id="heart-model" gltf-model="Heart.gltf" position="0 1.5 -5"
    scale="0.01 0.01 0.01" >
 </a-entity>
Enter fullscreen mode Exit fullscreen mode

The heart tag entity is added outside of the scene tag as we would like the flexibility of adding it programmatically.

Adding the animation.
Add the animation feature as in the example. Name the startEvents - 'collected'. Collected is the name of the fired event we will use to start the animation.

<a-entity id="heart-model" gltf-model="Heart.gltf" position="0 1.5 -5"
    scale="0.01 0.01 0.01"
    animation="property: rotation; to: 0 360 0; loop: true; easing: linear; dur: 2000"
    animation__collect="property: position; to: 0 0 0; dur: 300; startEvents: collected"
    animation__minimize="property: scale; to: 0 0 0; dur: 300; startEvents: collected">
</a-entity>
Enter fullscreen mode Exit fullscreen mode

Adding the score tag.
Add text tag inside a camera tag. This way it is visible for the user from every angle. Next, to collect the heart, add a cursor.

<a-camera>
      <a-text id="score-element" value="Score" position="-0.35 0.5 -0.8"></a-text>
      <a-cursor></a-cursor>
</a-camera>
Enter fullscreen mode Exit fullscreen mode

Last but not least, add a JavaScript file where we can code game actions and handlers.
Create a file, name it game.js and another html tag inside the html file:

<script src="game.js"></script>
Enter fullscreen mode Exit fullscreen mode

Full html file should be as follows:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>MR Valentines</title>
  <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
  <script src="https://rawgit.com/feiss/aframe-environment-component/master/dist/aframe-environment-component.min.js"></script>
</head>
<body>
  <a-scene>
    <a-camera>
      <a-text id="score-element" value="Score" position="-0.35 0.5 -0.8"></a-text>
      <a-cursor></a-cursor>
    </a-camera>

    <a-entity environment="preset:japan"></a-entity>
    <a-entity laser-controls></a-entity>
  </a-scene>

  <a-entity id="heart-model" gltf-model="Heart.gltf" position="0 1.5 -5"
    scale="0.01 0.01 0.01"
    animation="property: rotation; to: 0 360 0; loop: true; easing: linear; dur: 2000"
    animation__collect="property: position; to: 0 0 0; dur: 300; startEvents: collected"
    animation__minimize="property: scale; to: 0 0 0; dur: 300; startEvents: collected"></a-entity>

  <script src="game.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

For controlling the tags, fetch them from the DOM. One of the ways to do this is with the query selector. Fetch the a-scene tag, the heart model entity, and score element entity. Pay attention that when fetching a tag we use the full tag name without the symbol '#'. When fetching tag by id we use the symbol '#'. Notice the heart-model and the score-element query selector. The parameters are const and therefore will not change.

const sceneEl = document.querySelector("a-scene")
const heartEl = document.querySelector("#heart-model")
const scoreEl = document.querySelector("#score-element");
Enter fullscreen mode Exit fullscreen mode

The score value will change during the game. Define score parameters and define a function to update the score tag:

let score = 0;
function displayScore() {
  scoreEl.setAttribute('value', `Score: ${score}`);
}
Enter fullscreen mode Exit fullscreen mode

Since the heart entity is not part of the scene it will not appear in the screen unless we add it. Programmatically add it to the scene by cloning the tag and adding a random position. Add an event listener for pressing the mouse, or the MR controller and append it to the scene. Notice that you are now bonding the heart animation using the event name 'collected'. For an infinite game, bond the 'animationcomplete' event to the scaling animation with a new random position attribute. This will create the feeling of a new heart pop-up.

function randomPosition() {
  return {
    x: (Math.random() - 0.5) * 20,
    y: 1.5,
    z: (Math.random() - 0.5) * 20
  };
}
Enter fullscreen mode Exit fullscreen mode
function createHeart(){
  const clone = heartEl.cloneNode()
  clone.setAttribute("position", randomPosition())
  clone.addEventListener('mousedown', () => {
    score++;
    clone.dispatchEvent(new Event('collected'));
    displayScore();
  })
  clone.addEventListener('animationcomplete', () => {
    clone.setAttribute("position", randomPosition());
    clone.setAttribute('scale', '0.01 0.01 0.01');
  });
  sceneEl.appendChild(clone)
}
Enter fullscreen mode Exit fullscreen mode

To make it more fun we will add a 'for loop' for creating the heart 15 times:

for(let i=0 ; i<15; i++){
  createHeart()
}
Enter fullscreen mode Exit fullscreen mode

This is the complete JavaScript file:

const sceneEl = document.querySelector("a-scene")
const heartEl = document.querySelector("#heart-model")
const scoreEl = document.querySelector('#score-element');

function randomPosition() {
  return {
    x: (Math.random() - 0.5) * 20,
    y: 1.5,
    z: (Math.random() - 0.5) * 20
  };
}

let score = 0;

function displayScore() {
  scoreEl.setAttribute('value', `Score: ${score}`);
}

function createHeart(){
  const clone = heartEl.cloneNode()
  clone.setAttribute("position", randomPosition())
  clone.addEventListener('mousedown', () => {
    score++;
    clone.dispatchEvent(new Event('collected'));
    displayScore();
  })
  clone.addEventListener('animationcomplete', () => {
    clone.setAttribute("position", randomPosition());
    clone.setAttribute('scale', '0.01 0.01 0.01');
  });
  sceneEl.appendChild(clone)
}

for(let i=0 ; i<15; i++){
  createHeart()
}
displayScore()
Enter fullscreen mode Exit fullscreen mode

You are almost done. All you have to do is deploy:

Inside the project, create another folder with the same name as the project. Move all the project files into it. In VScode go to the project library, right-click on the web directory and choose Deploy to static Website. Make sure you have the Gen2 storage.

Alt text of image

Choose your subscription and the storage account that you created. You can also create a new storage account using VScode. When completed go to the Azure portal site and copy your website URL. This is how it should look:

Alt text of image

Another example is a personal blog. Check it here:

With the Microsoft Azure Cloud, you can share your game with friends. Without it, you can run it locally as well or host it on other platforms.

This game was build in collaboration with Uri Shaked.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .