🐦 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:
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.
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:
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.