loading...
Cover image for How to build a WebVR game with A-Frame
Microsoft Azure

How to build a WebVR game with A-Frame

adipolak profile image Adi Polak Updated on ・7 min read

🐦 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

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"
  }

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

Next, run:

npm install

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>

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

npm start

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>

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>

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>

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>

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>

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>

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");

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}`);
}

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
  };
}
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)
}

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()
}

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()

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.

Posted on by:

adipolak profile

Adi Polak

@adipolak

1 out of 25 influential women in Software Development according to Apiumhub. I am a software developer who would like to learn more!

Microsoft Azure

Any language. Any platform.

Discussion

markdown guide
 

Thanks so much! this is awesome - I've been working on this project http://ganeshvr.glitch.me/ but still learning on how to addEventListeners and being able to add javascript - this helps clear up so much! 🙏

 

Hi Ronak, thank you so much for your kind words. I am happy I was able to help you.
As I am highly impressed with your website, I have some questions for you. The first one, what is Ganesh Festival? it seems very happy and cheerful ( I hope I am not mistaken here). the second one is how did you make the beautiful motions of those purple stars? will you be sharing it on github?

 

Hi Adi! 🤗

   Every year thousands of ppl in india gather together to celebrate the birth of Lord Ganesh. Typically it last 9 days and usually some one from the neighborhood would get a statue and they will decorate the statue have sweets and  there will be evening prayers and celebrations.  
  • The whole idea was sort of bring that festival experience in VR.

p.s - it is networked so if two or more ppl visit the link at the same time - you can see each other's avatar :)

I don't have GitHub yet - I just put it together on glitch.com

  • The stars are particle component from a-frame .
    • https://www.npmjs.com/package/aframe-particle-system-component The official docs are here
    • https://aframe.io/docs/0.9.0/introduction/entity-component-system.html#including-the-component-js-file The Networked so its multiplayer experience
    • https://www.npmjs.com/package/networked-aframe

Hope it helps :)

 

Update
Apparently aframe 0.9.0 was broken early on Dec 2019.

To fix it, use 0.9.2 or 1.0.0 version insted:

Old:

 <script src="https://aframe.io/releases/0.9.0/aframe.min.js"></script>

New:

 <script src="https://aframe.io/releases/0.9.2/aframe.min.js"></script>
 

Looks intresting. Thanks for sharing!

 

Thank you, Martín! Will you give it a try ? :)