Developing an Immersive Game with A-Frame and Low Poly Models (Part 2)

In component one of this two-part tutorial, we created an A-Frame game using 3D models through Sketchfab and a physics engine. Whack-an-Imp works and it has nice landscaping however it still doesn’ t feel very immersive.

The lighting is wrong. The sky is 100 % pure white and the ground is real red. The trees don’ big t have shadows and there is no firelight coming from the cauldron. The moon has gone out so it must be night time, but we all don’ t see reflections associated with moonlight anywhere. A-Frame has provided us default lighting but it no more meets our needs. Let’ h add our own lighting.

Lighting

Change the colour of the ground to something more ground-like, a dark green.

  < a-plane color="#52430e"...
 
 

Add a darkish twilight sky:

  <! -- history sky -->
< a-sky color="#270d2c"> < /a-sky>
 
 

I did try adding fog for added mood, but it simply blocked the particular sky, so I took it away.

For the moonlight we are going to use a directional light . This means the light originates from a particular direction but is positioned definitely far away so that the light hits all of surfaces equally. For something like the particular moon, this is what we want.

  < a-entity light="type: directional; color: #ffffff; strength: 0. 5; "
position="31 eighty -50"> < /a-entity>
 
 

Here’ s what it appears like now:

with lighting

Hmm… We are getting there but it’ s still not quite right. The moonlight certainly reflects nicely off the best of the rocks, but the bottoms from the rocks and trees are too darkish to see. While this might be a realistic picture it doesn’ t feel like a spot that I would want to visit.

A common movie trick for capturing a night scene is to possess a colored light shining up to light up the undersides of objects without having making the scene so brilliant that the illusion of nighttime is definitely ruined. We can do this with a hemisphere light.

A hemisphere light gives us one colour above and one below. I utilized white for the upper and a kind of purplish dark blue for the decrease, at an intensity of 0. 4. Please experiment with different settings.

 <! -- hemisphere light going from white in order to dark blue -->
< a-entity light="type: hemisphere; color: white; groundColor: #5424ff; intensity: 0. 4"
> < /a-entity>
 
 

Now just one more thing. The particular fire under the cauldron should give off a warm red glow as well as the nearby rock should reflect this particular glow.

 < a-entity light="type: point; strength: 1 . 6; distance: 5; corrosion: 2; color: red"
position="0. 275 -0. 32 -3. 77"> < /a-entity>
 
 

This can be a red-colored point light, meaning they have a specific position and will decay along with distance. I set the corrosion to 2 and the intensity to at least one. 6. It is positioned just somewhat offset from the bottom of the cauldron so that we get a nice crimson reflection. I also set the distance in order to 5 so that only the very nearest rocks will get any of the red gentle.

Here’ s what looks like now. I think we lastly have a cool-looking scene. It feels just like a place where stuff is happening, along with secrets to explore.

lighting with up-lights

Shadows

There’ s just one more piece of illumination work to do. We need some dark areas. Shadows are expensive computationally speaking, and we only want to turn them upon for objects whose shadows we all actually care about.

Initial we must enable casting shadows in the light that will create them, the particular moonlight. Simply add castShadow: true towards the light attribute.

 < a-entity light="type: directional; color: #ffc18f; intensity: 0. five; castShadow: true; "
position="31 eighty -50"> < /a-entity>
 
 

Now add shadow="receive: true" towards the ground. All of the objects now immediately cast shadows onto the ground.

  <! -- the ground --->
< a-plane color="#52430e"
 static-body
 rotation="-90 0" width="100"
height="100" shadow="receive: true"> < /a-plane>
 
 

It’ s starting to feel like a real location.

Lighting with Shadows

In order to save cycles, the shadows is only going to be cast in an area the shadow frustum . To see this particular area set shadowCameraVisible to true for the light.

Audio

Just a few more things to shine up our game. Some sound. Lived-in worlds aren’ t noiseless. A summer night should have crickets or a breeze, the bubbling from the cauldron, and of course when we hit the particular imp it probably should make a complaint, loudly. To liven things up I discovered a few useful sounds at freesound. org .

First up: the particular nighttime sounds of crickets along with other creatures. I found a clip simply by freesound user sagetyrtle called October Night two . Since this clip includes background sounds I don’ big t want them to be positional. The gamer should be able to hear them from anyplace, and it should loop over and over. In making this happen I put the audio on the scene itself using the sound feature.

 < a-scene
...
sound="src: url(. /audio/octobernight2. mp3); loop: true; autoplay: true; quantity: 0. 5; "
>

 
 

Notice that I fixed the volume to 50% so it won’ t drown out the other sound clips.

Next we need an audio for the bubbles in the cauldron. I’ m using this sound called SFX Boiling Water by Euphrosyyn .

 <! -- cauldron -->
< a-entity gltf-model="#cauldron"...
sound="src: url(. /audio/boilingwater-loop. mp3); autoplay: true; cycle: true; "
> < /a-entity>
 
 

Again I had set the audio to cycle, but because it’ s mounted on the cauldron’ s entity rather than the scene, the sound will appear to come from your cauldron itself. Positional audio actually enhances the immersiveness of digital scenes. Granted, this boiling water impact is way overkill for the cauldron. Within real life the cauldron wouldn’ big t bubble as quickly or loudly, yet we want immersion, not realism.

Finally we need a sound over the imp. I chose this oops sound by metekavruk .

 < a-entity id='imp-model'...
sound="src: url(. /audio/gah. mp3); autoplay: fake; loop: false; "
> < /a-entity>
 
 

Each autoplay and loop are set to false due to the fact we only want the sound to try out when the imp is hit with all the weapon. Go down to the collide event handler and add this line to try out the sound on every collision.

  $("#imp-model"). components. sound. playSound();
 
 

The original files from freesound. org are in wav format, which is totally uncompressed and large. If you plan in order to edit the sound files then this is exactly what you want, but for distribution on the web we would like something far smaller. Be sure to transform them to MP3s first, which gives the 90% file size savings. On Mac pc and Linux you can use the ffmpeg device to convert them like this:

 ffmpeg -i boilingwater-loop. wav boilingwater-loop. mp3
 
 

More Polish

Creating the basic code may be the first 90% of building a game. Polishing the experience is the second 90%. Once i first built Whack-an-Imp I noticed it would get boring really quick. The only thing the player can do is wait around until the imp jumps out plus hit it. It would be more fascinating if every now and then something popped out there that the player shouldn’ t hit. Let’ s add a dragon’ s ovum.

Inside the ball enterprise we have a model for the imp. Alongside it add another entity known as egg-model, this time using a slightly altered sphere.

  <! -- the golf ball contains two models that we exchange -->
< a-entity id='ball'
position="0. 1 -4"
 rotation="0 zero 0"
dynamic-body="shape: sphere; sphereRadius: zero. 3; mass: 4"
>
< a-entity id='imp-model' gltf-model="#imp" position="0 -0. 4 0"
sound="src: url(. /audio/gah. mp3); autoplay: false; loop: fake; "
> < /a-entity>
< a-sphere id='egg-model' radius="0. 25" segments-height="8" segments-width="8"
scale="1 0. 6 zero. 8"
material="color: purple; flatShading: genuine; emissive: red; emissiveIntensity: 0. 2"
sound="src: url(. /audio/cowbell. mp3); autoplay: false; loop: false; "
> < /a-sphere>

< /a-entity>
 
 

To make the sphere seem more magical I gave this a purple colored material along with flat shading, but also set the particular emissive color to red. Usually a material only reflects gentle that comes from a light source, but a good emissive color lets the materials produce it’ s own lighting, even in the dark. In effect, this glows. I also added a cowbell sound from pj1s for once the player hits the egg.

Note in the code over that I moved the dynamic-body from the imp towards the surrounding ball entity. This is because we want exactly the same physics behavior regardless of which item is hit. However , the imp model is slightly offset and can stick outside of the sphere bounds, and so i adjusted the position by -0. four in the y direction.

Now we need to update the resetBall occasion handler with a boolean indicating whenever we should show the imp or maybe the dragon’ s egg.

  let showImp = true
const resetBall sama dengan () => 
        clearTimeout(resetId)
        $("#ball").body.position.set(0, 0.6,-4)
        $("#ball").body.velocity.set(0, 5,0)
        $("#ball").body.angularVelocity.set(0, 0,0)
        showImp = (Math.floor(Math.random()*4)!==0)
        $("#imp-model").setAttribute('visible',showImp);
        $("#egg-model").setAttribute('visible',!showImp);
        hit = false
        resetId = setTimeout(resetBall,6000)
    
 
 

We also need to make the collide-handler play the correct sound and decrement the particular score by 10 if you unintentionally hit the egg.

  on($("#weapon"), 'collide', (e)=> 
        const ball = $("#ball")
        if(e.detail.body.id === ball.body.id && !hit) 
            hit = true
            if(showImp) 
                $("#imp-model").components.sound.playSound();
                score = score + 1
             else 
                $("#egg-model").components.sound.playSound();
                score = score - 10
            
            $("#score").setAttribute('text','value','Score '+score)
            clearTimeout(resetId)
            resetId = setTimeout(resetBall,2000)
        
     )
 
 

More Details

Let’ s fix a few final information before we go: Display the particular score in white so we can easily see it in the dark, turn off physics debugging in the a-scene , and remove the cursor inside the digital camera. We don’ t need the particular cursor anymore because we have the particular staff to indicate where the camera is certainly pointed.

Final Game

Whack-an-Imp is complete! It’ s time for you to test it out. We already know functions on the desktop. Here it is upon my phone.

phone screenshot

Full VR Headset

The only way to test VR is to operate it on real hardware. We ran it on my Home windows Mixed Reality headset and it appears pretty good. The image and positional sound work quite well. I definitely possess a feeling of being present. However , the particular interaction feels very awkward since the staff is attached to my head. Rather, I want to use the staff with the actual 6dof control. We can do this by moving the particular staff inside of a new entity along with laser-controls .

  < a-entity id='laser' laser-controls="hand: left" raycaster="showLine: fake; " line="opacity: 0. 0; ">
< a-entity rotation="-105 0" position="0 -3. 5" id='weapon' static-body="shape: sphere; sphereRadius: 0. three or more; ">
< a-entity scale="1. eight 1 . 8 1 . 8" position="0 1 . 5 0">
< a-entity position="2. 3 -2. 7 -16. 3" gltf-model="#staff" > < /a-entity>
< /a-entity>
< /a-entity>
< /a-entity>
 
 

The particular laser-controls will automatically attach its items to the user’ s six examples of motion controller. These are typically large handsets that come with PC headsets such as the Vive, Rift, and MR headphones. The laser-controls component also works with 3 degrees of freedom controllers like the types that come with Google Daydream and Equipment VR.

This produces a new problem. The game now works together with a controller in a traditional VR headset, but it won’ t utilize a phone anymore. There is no canonical fix for your problem, so I have chosen to enable each behaviors and simply enable and deactivate the correct one at runtime. To get this done we’ ll need to change our own markup slightly.

Include the laser group to the picture, then change the id=weapon of both employees to class='weapon' , and add an extra class to get gaze to the one inside of the camera plus dof6 to the one inside of the laser.

Here is the final result.

  < a-entity digital camera look-controls position="0 1 . 5 0">
< a-text id="score" value="Score" position="-0. 2 -0. 5 -1" color="white" width="5" anchor="left"> < /a-text>
< a-entity rotation="-90 0" position="0 -4" class='weapon gaze' static-body="shape: sphere; sphereRadius: 0. 3; ">
< a-entity position="2. 3 -2. 7 -16. 3" gltf-model="#staff" > < /a-entity>
< /a-entity>
< /a-entity>

< a-entity id='laser' laser-controls="hand: left" raycaster="showLine: false; " line="opacity: 0. 0; ">
< a-entity rotation="-105 0" position="0 zero -3. 5" class='weapon dof6' static-body="shape: sphere; sphereRadius: 0. 3; ">
< a-entity scale="1. 8 one 8 1 . 8" position="0 1 ) 5 0">
< a-entity position="2. 3 -2. 7 -16. 3" gltf-model="#staff" > < /a-entity>
< /a-entity>
< /a-entity>
< /a-entity>
 
 

Let’ t add some functions to turn a set of regulates on or off. The setEnabled perform below sets the visible house of the desired element and also transforms physics off of its static entire body. Then the switch6DOF and switchGaze functions call setEnabled with all the right parameters.

  function setEnabled(sel, vis) 
        const elem = $(sel)
        elem.setAttribute('visible', vis)
        if(elem.components ['static-body'] ) 
            const sb = elem.components ['static-body'] ;
            if(vis) 
                sb.play()
             else 
                sb.pause()
            
        
    

 function switch6DOF() 
        $('#laser').setAttribute('visible',true)
        setEnabled('.weapon.gaze',false)
        setEnabled('.weapon.dof6',true)
    


 function switchGaze() 
        $('#laser').setAttribute('visible',false)
        setEnabled('.weapon.gaze',true)
        setEnabled('.weapon.dof6',false)
    

 
 

Perform Whack-An-Imp

Now we all just need to decide when to use which usually set of components. We could check if these devices is a mobile device but that will won’ t handle the desktop computer case when the headset isn’ capital t connected. Instead we should look for the particular ‘ enter-vr’ event and then find out if a headset is connected. When it is, switch to 6DOF mode, otherwise make use of gaze mode.

  on($('a-scene'), 'enter-vr', ()=> 
        if(AFRAME.utils.device.checkHeadsetConnected()) 
            switch6DOF()
         else 
            switchGaze()
        
     )
on($('a-scene'), 'exit-vr', switchGaze)

 //always start up in gaze mode
on($('a-scene'), 'loaded', switchGaze)
 
 

And with that, Whack-an-Imp will always adjust to the current situation. The player can change in and out of VR mode effortlessly. This is the hallmark of web programs, applied to a mixed reality encounter: responsive design .

And with that, we conclude this particular project. You can play with the live version on my website and look for the program code on GitHub. Please remember to submit your own entry to the current WebVR challenge . There are various great prizes to be had, and lots of understanding as well. Let us know how it goes…

I am an author, researcher, plus recovering engineer. Formerly on the Golf swing Team at Sun, the webOS team at Palm, and Htc Research. I spread the word great user experiences. I live in sunlit Eugene Oregon with my wife plus genius Lego builder child.

A lot more articles by Josh Marinacci…

If you liked Developing an Immersive Game with A-Frame and Low Poly Models (Part 2) by Josh Marinacci Then you'll love Web Design Agency Miami

Add a Comment

Your email address will not be published. Required fields are marked *

Shares