Driving Simulator

View Project View Code Clone Project

What You will learn:

Requirements for this tutorial:

Goals:

Tutorial Content:

  1. What is A-Frame?
  2. Basic A-frame scene (HTML)
  3. Environment: Lighting & Shadow & Sky & Ground (HTML)
  4. First Map (JS)
  5. 3D models
  6. Camera (HTML)
  7. Car Movement(JS)
  8. Vertical Movement(JS)
  9. Controller Input (JS)
  10. Randomly Generated Map (JS)

1. What is A-Frame?

A-Frame Logo

A-frame is A web framework that makes building 3D/AR/VR experiences easy.

For additional information and different demos of what people have already created check out the official A-frame website. For our project today we will be using version 0.8.0 of A-Frame, so when looking at the documentation make sure to be on the correct version.

The offical A-Frame documentation: https://aframe.io/

Back to Top


2. Basic A-frame scene:

Before we start, play around with the demo! Drive around, if this is something you are interested in making then you've come to the right place. Open up the demo code on a separate tab, create a new coding space and let's jump right into A-Frame!

Installation (Don’t worry it's easy!):

For the method we're using there is no real installation needed, all you need to do is add this script into the head element of your HTML file:

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

And the element in the body of your HTML:

<body>
	<a-scene>
	</a-scene>
 </body>

And you’ve made your first scene! Now let’s add some objects:

There are many A-Frame primitives but we’ll just start with a box. Insert it into your a-scene as shown:

<body>
	<a-scene>
		<a-box></a-box>
	</a-scene>
</body>

This has created a default white 1 meter by 1 meter by 1 meter box. By setting the attributes you can change the box to anything you want.

For example, here is a red, 0.25m by 1.5m by 2m box scaled up by 2 on the y-axis:

<a-box color="red" scale="1 2 1" width="0.25" height="1.5" depth="2"></a-box> 

If you don’t see your box at first use your mouse to look down, if you didn’t set a position it should appear right where the default camera is placed (0, 0, 0).

Additionally, WASD keys are set up already in the default camera for basic movement, you can use these for now but we’ll adjust them later.

MUST DO: So you can see your progress as we begin to make scene changes, start by making a simple scene of blocks, spheres, or whatever you like. Just have fun and play with it!

Here's a small list of primitives:

For more go check out the official A-Frame documentation!

Helpful tools!:

Click Here to learn about A-Frame Inspection mode

When inside your scene press Ctrl Shift i on Windows, or Control Option i on Mac to enter A-Frame Inspection mode. Very helpful when you want to easily test how different changes would look.

Here's a link to other projects made using A-Frame: https://stemkoski.github.io/A-Frame-Examples/

Now that you have a basic understanding of A-frame, read through the demo code and try to understand it, most of it is javascipt anyways.

Back to Top


3. Environment: Sky & Ground & Lighting & Shadow

Sky & Ground

To add in the sky and ground, we’ll use more A-frame primitives: a-plane and a-sky.

<!-- Sky -->
<!-- For if you have an image 
<a-sky src="#sky_image" rotation="0 0 -180"></a-sky> -->
<a-sky color="blue"></a-sky>

<!-- Ground -->
<a-plane material="color: #999999" rotation="-90 0 0" scale="75 75 0.5"></a-plane>
Bonus: How do these work?

But how does an a-sky cover our world? really you can think of it as a very big inverted sphere, showing a color or image to the interior.

And the a-plane is pretty self-explanatory, it's a flat plane we turn -90 degrees on its x-axis and scale up to the size of our world.


With either of these objects you can use an image or a color, so adjust these to your desire.

Lighting:

In A-Frame, there are 5 types of light: ambient, directional, hemisphere, point, and spot.

In every A-Frame scene there are lights by default and the are set as the following values:

<!-- Default A-Frame lighting -->
<a-entity light="type: ambient; color: #BBB"></a-entity>

<a-entity light="type: directional; color: #FFF; intensity: 0.6" position="-0.5 1 1"></a-entity>

As practice, let's break down this code. First, you've probably noticed that these both have the tag <a-entity> not <a-light>. And what even is an a-entity? An a-entity is an empty object that it is up to the user to define its components. For example, if we want our a-entity to be a box then we need to add the geometry attribute and set it to primitive: box. Likewise, by adding the attribute light we give it the properties of an <a-light>.

Next, let's look at the attributes and see how these lights are different. The first one, is an ambient light with a light gray color and the second is a directional light with a white color, intensity, and position/direction ratio.

Pretty simple, right? Good! In time it will be just as smooth as reading.

However, these are just the default lights, to see how the scene changes when you alter their properties, make your own and play around with them a bit.

Note: When you create a light element, A-Frame automatically disables the default lights. They can also be disabled in the a-scene element by hand.

The lights in the demo are set as:

<!-- Light entities-->
<!--Light Ambient-->
<a-entity light="type: ambient; color: #BBB; intensity: 0.7"></a-entity>

<!--Light Directional: ex. sun -->
<a-entity light="type: directional; color:#FFFFFF; intensity:1.5" position="28.5 38.1 16"></a-entity>

Play around, insert different values and see how this changes the scene.

Shadows:

We have a light source now, so where are the shadows? Shadows have to be implemented separately but don't worry it's not too hard!

To enable shadows, you must do two things:

  1. Add the castShadow to your light attribute from your light source (point, spot, or directional)
  2. Add the shadow component to every object that you want shadows to affect, and specify whether you want this object to cast or receive shadows.

To do more configuring with how the shadows render, add the shadow attribute to your scene.

<!-- Example -->
<a-scene shadow="autoUpdate:true; type: pcf">
	<!-- -->
</a-scene>

As always, if you want more information check out the offical A-Frame documentation on shadows.

Bonus: As our scene gets bigger we also need to expand the area that shadows cover. To visualize this add shadowCameraVisible: true to your light attribute, or go into the A-Frame inspector (Ctrl Shift i on Windows, or Control option i on Mac), click on your light source on the left side bar, and check shadowCameraVisible. You should see a rectangle tilted in the angle of your light source, that zone is where shadow appear. Make the rectangle bigger or smaller with shadowCameraBottom, shadowCameraLeft, shadowCameraRight, and shadowCameraTop.

if you have trouble with these settings, the demo's is set to:

<a-entity light="type: directional; color:#FFFFFF; intensity:1.5; castShadow: true; shadowBias:-0.001; shadowCameraBottom: -50; shadowCameraLeft: -50; shadowCameraRight: 50; shadowCameraTop: 50" position="28.5 38.1 16"></a-entity>

Now that the basics of our enviornment is set let's start on our city!


4. First Map - basic city

Our first section of JavaScript, yay! As this gets more difficult I would recommend opening the web inspector when testing to see the console for debug messages or errors (F12 or Crtl Shift I).

In this section, we will be working towards this:

SampleMap

It may look a bit childish but these blocks are just place holders until we get to the 3D objects section then we can make our scene a lot prettier. Now how do we get there?

As you've probably learned by now, placing every building into your scene individually very tedious and will be difficult to change later in development as your html document becomes more cluttered. To make this easier, let's use javascript to write the HTML for us.

Let's start in our HTML document by creating a container element within our a-scene. This will be the container for all of the map objects: buildings, roads, etc.

<!-- Holds all the Building entities-->
<a-entity id="map" shadow="cast:true; receive:true"></a-entity>

Remember: the shadow component is inherited by child objects, so instead of adding the shadow component to every map object, adding it to the map will efficiently do the same thing.

Next, in the javascript file, let's access the map entity we've just created:

// Accesses the map entity 
let map = document.querySelector("#map");

Now that we have set up and have access to our container, we need to place objects into our map. As shown in the example above, our city as a 2D grid from a bird's-eye view. A 2D grid can be easily diplayed as a 2D array.

Sample: this is a 9 by 9 2D array, where 1 equals a building and 0 equal free space:

var city = [[1, 1, 1, 1, 1, 1, 1, 1, 1],
			[1, 1, 1, 0, 0, 0, 0, 0, 1],
			[1, 0, 0, 0, 1, 1, 1, 1, 1],
			[1, 0, 1, 0, 0, 0, 0, 0, 1],
			[1, 0, 1, 0, 1, 1, 1, 0, 1],
			[1, 0, 1, 0, 1, 0, 0, 0, 1],
			[1, 0, 1, 0, 1, 1, 1, 0, 1],
			[1, 0, 1, 0, 1, 0, 0, 0, 1],
			[1, 1, 1, 1, 1, 1, 1, 1, 1]]

If its difficult to see the difference between 1 and 0, here's the same array of numbers but color-coded:

SampleGrid

Now that we have the layout of our city, we need to access each value from our array, turn it's index into a position, and then make the HTML that will be displayed in our a-scene.

  1. Access every value

Test: figure out the code below:

setup();

function setup () {
	map.innerHTML="";
	for (let i = 0; i < city.length; i++) { 
		for (let j = 0; j < city[0].length; j++) {
			console.log(city[i][j];)
		}
	}
}
Click Here for Explaination

The code above interates through each row, and in each row interates through every column, accesses each element in the city array and prints it to the console ( which found in the web inspector).

In addition, this process is in a function is so it could be reused multiple times which will be helpful later in Section 10: Randomly Generated Map..

  1. From index to position

Printing the value to the console is helpful to make sure that we are on the right track but that doesn’t place any objects. We need the index in the array to reflect a building’s position in our scene.

Let's say that each box on our grid is 5 meters by 5 meters, and the starting point is the middle of our grid, then our process looks like this:

setup();

function setup () {
	map.innerHTML="";
	for (let i = 0; i < city.length; i++) { 
		for (let j = 0; j < city[0].length; j++) {
			
			let x = (i * 5) - (2.5 * (city.length-1))
			
			// y is set in each building type (can't be found in a 2D array)
			
			let z = -j * 5 + (2.5 * (city.length-1))
			
			let val = city[i][j]; // access the object type
			
			addBuilding(x, z, val);
		}
	}
}

Now that we have all the information to set up our objects, let's have our JavaScript write the HTML code with the help of helper functions:

Since we don't have very many types of objects yet, all we should have is:

addEntity(id, position, scale, rotation) {
	const entity = document.createElement('a-entity');
    entity.setAttribute('id', id);
    entity.setAttribute('geometry', 'primitive: box');
    entity.setAttribute('position', position);
    entity.setAttribute('scale', scale);
    entity.setAttribute('rotation', rotation);
    map.appendChild(entity);
}

addBuilding(x, z, val) {
	if (val == 0) { // empty space
		// no object made
	} else if (val == 1) {
		y = 2.5
		addEntity("#building", `${x} ${y} ${z}`, "5 4 4", "0 0 0"}
	} // else if (val == next val)
}

If you have your 2D array, city, all filled then you should have a small city of multi-colored blocks.

Bonus Problems:

If you get stuck on any of these challenges, read through the demo code, look through the official A-Frame documentation, or Google it, but don't give up!


5. 3D models & Assets

Right now our city looks like a box of crayons, to give our world more flavor and so users know that its really a city, lets give more detail to our world.

Within your project make an Asset folder, that's where you will keep all your images and 3D objects.

Assets Folder

Now in your HTML document, let's load some assets. Create an a-assets element in your a-scene, this is where you will load all your assets. For images use the img tag and for 3D objects use a-asset-item:

<a-assets>
	<a-asset-item id="asset-id" src="Assets/asset.gltf"></a-asset-item>  	
	<img id="image-id" src="Assets/image.jpg">
</a-assets>

A-frame takes three types of 3D models: glTF(.gltf or .glb), OBJ(.obj), and COLLADA(.dae). But glTF is recommended by A-Frame and what was used for the demo.

If you want to use the resources from the demo: View Code

If you want to make your own 3D models Blender is a very helpful tool for that!

Blender Logo

For Blender Tutorials I’d recommend:

But the best tip for learning Blender is really just to play with it. Learn keybindings, get comfortable with camera movement, and practice! If you get stuck, GOOGLE IT. There are many Blender masters out there who can help you.

Back to Top


6. Camera:

The camera is a very important part any video game experience. It dictates what information the player has visual access to at any time.

In A-Frame, the default camera takes care of movement as well (As seen in at the end of part 2: Basic A-frame scene).

Bonus Section: Default Camera Settings
<a-entity camera="active: true" look-controls wasd-controls position="0 0 0" data-aframe-default-camera></a-entity>

Let's break down this code:

  • Camera attribute, because this element uses the <a-entity> instead of a <a-camera>
    • in addition, active: true mean that this camera is the one currently being used by the player, useful when you have multiple camera elements
  • Is look-controls and wasd-controls, as their names suggest enables the ability to look around and move. Just like the default lights, when you add your own camera the default camera is disabled.

In this section we will:

  1. Set up the camera and camera rig (the car part)
  2. And then we need to separate the look-controls & wasd-controls, further Car movement will be worked on next section.

SampleCamera

The image above is what we want completed by the end of this section.

If you haven't already, go into the demo code and download the car object or make your own and add it into your a-assets (Section 5: 3D models & Assets).

  1. Set up the camera and camera rig

From what we saw in the description above, the camera rig is displayed by the car, and holds the camera object:

<a-entity id="car-object">
	<a-entity camera id="camera"></a-entity>
</a-entity>

Optional: inside the camera element you can add a <a-cursor> element to get the circle you see in the demo in the middle of your screen.

  1. Separate Movement and Look Controls

Now to set their attributes:

<!-- Camera Rig / Car / Controls are held here -->
<a-entity id="car-object" movement-controls="speed:0.3;" gltf-model="#car" position="0 0.08 0" scale="0.5 0.5 0.5" rotation="0 0 0">
	<!-- Camera -->
	<a-entity camera id="camera" position="-1 2 0.6" look-controls active="true">
    	<a-cursor></a-cursor>
    </a-entity>
</a-entity>

In the next section, we will work on car movement to replace the default movement-controls.

Back to Top


7. Car Movement:

In this section we will go over how to make our own component in A-Frame: extended-wasd-controls. But be warned, there will not be as much instruction for this section. Try your best!

Here's the offical documentation for Writing a Component.

Before we start, create a new JavaScript file and name it movement_script.js or something related to movement.

AFRAME.registerComponent('extended-wasd-controls', {
	
}

Since there is a lot to do, let's break up our work load into parts:

  1. Set our defaults
  2. Helper Functions
  3. Initization function : Initalize variables and event listeners
  4. tick function : What happens every frame, updating our vectors and movement
  5. Set boundaries

Defaults

First, what controls do we need?

To define the properties of our component we'll use a schema. So far we should have:

AFRAME.registerComponent('extended-wasd-controls', {
	schema: 
	{
		moveForwardKey:     {type: 'string', default: "W"},
		moveBackKey:    {type: 'string', default: "S"},
		// Basic Structure:
		// propertyName: {type: '', default : Value},
		// functionName: function(input) {
		//	**function contents**
		//}
	}
}

TODO: set all of the properties listed above. Once done check with the demo code.

Helper functions

Let's start with some helper functions:

Note: In our initization function we will initalize a set for keys currently pressed, named keyPressedSet.

These can be seen in the demo code, please read through it for a better understanding as we will be using them later.

Init function:

In the initization function we need to set up our event listeners for key presses and all our move vectors using the help of our newly made helper functions.

init: function()
	{
		
		// register key down/up events 
		//  and keep track of all keys currently pressed
		
		this.keyPressedSet = new Set();
				
		let self = this;
	
		document.addEventListener( "keydown", 
			function(eventData) 
			{ 
				
				var name = eventData.key;
				var code = eventData.code;
				self.registerKeyDown( self.convertKeyName(eventData.key));
				
				// Gear changes and Camera switches are check on HERE
			}
		);
		
		document.addEventListener( "keyup", 
			function(eventData) 
			{ 
				self.registerKeyUp( self.convertKeyName(eventData.key));
			} 
		);

		// movement-related data

		this.moveVector  = new THREE.Vector3(0,0,0);
		this.movePercent = new THREE.Vector3(0,0,0);
		// x = forward/backward
		// y = up/down
		// z = left/right

		this.rotateVector  = new THREE.Vector2(0,0);
		this.rotatePercent = new THREE.Vector2(0,0);
		// y = turn angle
		// x = look angle

		// used as reference vector when turning
		this.upVector = new THREE.Vector3(0,1,0);
		
		// current rotation amounts
		this.turnAngle = 0; // around global Y axis
		this.lookAngle = 0; // around local X axis
		
		// allows easy extraction of turn angle
		this.el.object3D.rotation.order = 'YXZ';
	}

TODO: Read through the code above and try to get a brief understanding of it we will be using it in the rest of this section.

Test: Write the code for listening for gear changes and camera switches with what you know so far and the helper functions we made.

To check your work look into the demo code

tick function

The tick function runs every frame, so this function is in control of changing the movement vectors established in our init function:

tick: function (time, timeDelta) 
{
	let moveAmount = (timeDelta/1000) * this.data.moveSpeed;
	
	// need to convert angle measures from degrees to radians
	let turnAmount = (timeDelta/1000) * THREE.Math.degToRad(this.data.turnSpeed);
	let lookAmount = (timeDelta/1000) * THREE.Math.degToRad(this.data.lookSpeed);
	let maxLookAngle = THREE.Math.degToRad(this.data.maxLookAngle);
	
	// rotations
	
	// reset values
	let totalTurnAngle = 0;
	let totalLookAngle = 0;
	
	// ---Gamepad Inputs for button will go here---

	// need to reset rotatePercent values
	// when querying which keys are currently pressed
	this.rotatePercent.set(0,0);

	if (this.isKeyPressed(this.data.turnLeftKey))
		this.rotatePercent.y += 1;
	if (this.isKeyPressed(this.data.turnRightKey))
		this.rotatePercent.y -= 1;
					
	// ---Gamepad Inputs for turning will go here---
			

	if ( this.data.currentGear != 1) // No turning while parked
	{
		this.turnAngle += this.rotatePercent.y * turnAmount;
		this.el.object3D.rotation.y = this.turnAngle;
	}

	// translations

	// this only works when rotation order = "YXZ"
	let finalTurnAngle = this.el.object3D.rotation.y;
	//console.log(finalTurnAngle);
	
	let c = Math.cos(finalTurnAngle);
	let s = Math.sin(finalTurnAngle);

	// need to reset movePercent values
	// when querying which keys are currently pressed
	this.movePercent.y = 0;
	
	if (this.movePercent.x -0.03 >= 0) { this.movePercent.x -= 0.03; } 
	if (this.movePercent.x +0.03 <= 0) { this.movePercent.x += 0.03; } 
	if (this.movePercent.x < 0.03 && this.movePercent.x > -0.03) { this.movePercent.x = 0; }

	// Moving Backwards and Forward 
	if (this.isKeyPressed(this.data.moveBackKey)) {
		//console.log(this.data.moveSpeed);
		if (this.data.currentGear == 2 && this.data.moveSpeed-1 > 0) { // Drive
			this.data.moveSpeed = 0;
		}
		this.movePercent.x = 1;
	}
			
	if (this.isKeyPressed(this.data.moveForwardKey)) {
		if (this.data.moveSpeed < 10) {
			this.data.moveSpeed += 0.1;
		}
		this.movePercent.x = -1;
	}
			
	// Where gamepad input for movement will be added in

	// forward(-x) direction: [ -s,  0, -c ]
	//   right(-z) direction: [  c,  0, -s ]
	//      up(y) direction: [  0,  1,  0 ]
	// multiply each by (maximum) movement amount and percentages (how much to move in that direction)
	
	if (this.data.currentGear == 1) { // Parked
		// no change in movement
	} else if (this.data.currentGear == 2) { // Drive
		// Only move forward
		if (this.movePercent.x > 0) {
			this.movePercent.x = 0;
		}
		
		this.moveVector.set( -s * this.movePercent.z + c * this.movePercent.x, // x
							  1 * this.movePercent.y, // y
							 -c * this.movePercent.z - s * this.movePercent.x ).multiplyScalar( moveAmount ); // z
	} else  if (this.data.currentGear == 3) { // Reverse
		// Only move backwards
		if (this.movePercent.x < 0) {
			this.movePercent.x = 0;
		}
		
		this.moveVector.set( -s * this.movePercent.z + c * this.movePercent.x, // x
							  1 * this.movePercent.y, // y
							 -c * this.movePercent.z - s * this.movePercent.x ).multiplyScalar( moveAmount ); // z
	}

	// Update Movement
	this.el.object3D.position.add( this.moveVector);
}

Set boundaries

I'm sure you've noticed that you can drive straight throught buildings and way off the map, and that's not right so let's set some game boundaries.

For now, copy and paste the 2D array of our map into movement_script and turn it into ones and zeros (one for wall and zero for empty).

TODO: In the schema write the function, isEmpty that will take a position in the scene and turn it into a index on our 2D array. Once done check with the demo code.

For reference, here's how the index was turned into a position in the map_script.js:

for (let i = 0; i < city.length; i++) { 
	for (let j = 0; j < city[0].length; j++) {
		
		let x = (i * 5) - (2.5 * (city.length-1))
		// y is set in each building type (can't be found in a 2D array)
		let z = -j * 5 + (2.5 * (city.length-1))
		
		let val = city[i][j]; // access the object type
	}
}

Next, at the end of the movement_script before the movement vectors are added to the position, create a new variable that will find the position that the car will be at.

// Value of new location
	let loc = this.isEmpty(-s * this.movePercent.z + c * this.movePercent.x + this.el.object3D.position.x, -c * this.movePercent.z - s * this.movePercent.x + this.el.object3D.position.z) 

With this new variable and the helper function we've just made, now we can check if we are heading into a wall.

TODO: Stop movement if the car is hitting a wall. Check in with the demo code once you are done.

Back to Top


8. Vertical movement:

In this section, our goal is ramps: RampSample

Lets break our goal down:

  1. The ramp is a object placed by the 2D array, city
  2. Car's vertical position changes
  3. Car is rotated upwards

Create a 3D Ramp Object

In the map_script, add a ramp object and place it into the 2D array:

...
} else if (val == __) { // Whatever value you haven't used yet
let y = 0;
addEntity("road_obj", 0, "#road", `${x} 0.1 ${z}`, "5 5 0.5", "-90 0 0")
addEntity("ramp", 2, "blue", `${x} ${y} ${z}`, "3 4 1", "80 -90 0")
}

Note: depending on how you have set up the addEntity function the inputs changes.

In HTML the code above is:

<a-plane id="road_obj" src="#road" position="${x} 0.1 ${z}" scale="5 5 0.5" rotation = "-90 0 0"></a-plane>

<a-entity id="ramp" geometry="primitive:box" color="blue" position="${x} ${y} ${z}" scale="3 4 1" rotation="80 -90 0"></a-entity>

In the movement_script, place 2 where you placed the ramp in the other 2D array (In the randomly generated map section we will connect these arrays but for now keep them seperate).

vertical position & rotation change

Just like how the car stops when about to hit a wall, the car will rise when about to hit a ramp:

let car = document.querySelector("#car-object")

// ... checks walls

if (loc == 2) // loc = the location that we will be in
{
	// console.log(this.el.object3D.position.y);
	if (this.el.object3D.position.y < 0.7) 
	{
		car.setAttribute("rotation", "0 0 -10")
		this.movePercent.y += 0.4;
	}
} else {
	if (this.el.object3D.position.y > 0.08)
	{			
		this.movePercent.y -= 0.3;
		car.setAttribute("rotation", "0 0 0")
	}
}

9. Controller Input:

The type of player input really affects the game. It constricts what players can and can't do. Think about multiplayer console games, because controllers don't have a full keyboard text communication is difficult aside from premade responses. Or what about phone games, how they’re adapted to have more touch or shake interactions.

ControllerSample

--Note: Controller input is not part of A-Frame, it can be implemented in any webpage or web game you would like. In addition this part will depend on what kind of controller you have.

To Test Controller Input: https: https://gamepad-tester.com/

For More Information about the Gamepad API: https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API

In this section we need to accomplish 2 things:

  1. Check if/which controllers are connected
  2. Map the controller inputs to movement

Check for Controllers

Above the movement component we made in movement_script, lets add a few event listeners to the window to check for controllers:

var gamepads = [];

window.addEventListener("gamepadconnected", function(e) {
		console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
	    e.gamepad.index, e.gamepad.id,
	    e.gamepad.buttons.length, e.gamepad.axes.length);
	    gamepads[e.gamepad.index] = e.gamepad;
	    console.log(gamepads);
	});


window.addEventListener("gamepaddisconnected", function(e) {
		console.log("Gamepad disconnected from index %d: %s",
   		e.gamepad.index, e.gamepad.id);
   		delete gamepads[e.gamepad.index];
   		console.log(gamepads);
	});

Map Inputs to Actions:

As you can see the on the example image shown at the start of this section, There are two kinds of inputs for controllers: buttons and axes(analog sticks).

Buttons give the input of 0 or 1, off or on. Axes on the other hand input 0 to 1 and everything inbetween. Given the type of input, let's assign the buttons and axes to functions.

Let's start with our axes, our two analog sticks:

// Within the tick function, where turning is checked
if (gamepads.length > 0 && (navigator.getGamepads()[0].axes[2] > 0.4 || navigator.getGamepads()[0].axes[2] < -0.4)) {
	this.rotatePercent.y = -navigator.getGamepads()[0].axes[2]
}

// Within the tick function, where movement is checked
if (gamepads.length > 0 && (navigator.getGamepads()[0].axes[1] > 0.4 || navigator.getGamepads()[0].axes[1] < -0.4)) {
	this.movePercent.x = -navigator.getGamepads()[0].axes[1];
}

The first part of the if condition is pretty standard: check if there are any gamepads connected at all. The second is more confusing, but this is because even when the analog sticks look like they are in the center and not moving, it is slightly tilted. Thats why we need to set a boundary, only when the movement of the analog stick is clearly intentional will the car will move.

Next, our buttons:

// Insert this into your code right before rotation checks
if (gamepads.length > 0) {
	if (navigator.getGamepads()[0].buttons[1].pressed) { this.switchGear(3); }
	if (navigator.getGamepads()[0].buttons[2].pressed) { this.switchGear(1); }
	if (navigator.getGamepads()[0].buttons[3].pressed) { this.switchGear(2); }
			
	if (navigator.getGamepads()[0].buttons[12].pressed) { this.switchView("F"); }
	if (navigator.getGamepads()[0].buttons[13].pressed) { this.switchView("R"); }
}

Now, you should be able to move your car around with your controller!

Back to Top


10. Randomly Generated Map Algorithm:

What steps do you take when you make a random maze? Do you start by making a main path from start to end and then make other branches?

Before we create this algorithm let's do some set up. First, make a new JavaScript document and name it map_maker.js, now we have our blank canvas to start on.

Making the Map:

Well for our city map we have some prerequisites:

We can do the first part pretty easily. The locations of the border walls and the exits/entrances never change so, we can have those pre-placed.

function cleanUp() {
	for (let i = 0; i < city.length; i++) {
		for (let j = 0; j < city[0].length; j++) {
			let value = city[i][j]; 
			
			if (i == 0 || j == 0 || i == 14 || j == 14) {
				if (i == 0 && j == 7) {
					city[i][j] = 0; // entrance/exit
				} else if (i == 14 && j == 7) {
					city[i][j] = 0; // entrance/exit
				} else {
					//city[i][j] = whatever value set to border;
				}
			} else {
				city[i][j] = 1; // building
			}
			
			
		}
	}
}

Now everything, aside from the border and exits is set to a wall and we need to carve a path out.

The method we will be using is Recursion Division (with some slight differences since we are not making a maze):

Recursive_maze.gif

** Recursion Division → if these terms are new to you then you can think about it like divide and conquer. Divide up the problem and start with smaller portions.

Let's say our map is a 2d grid, x being the width of our map and y being the height:

Lets break this down into two questions:

  1. What specifically do we want the program to do?
    • We want a to set empty space to a road object from point A to point B on our city array
  2. what does the program need?
    • We need to give it a start position and an ending position and which column or row it is on.

To access one specific row or column to pave our road, we have to keep one of the indexes the same while the other one increments -- singular for-loop time!

function verticalRoad (maxY, minY, x) {
	for (let i = minY; i < maxY; i++) {
		city[i][x] = 5; // Or whatever your road value is
	}
}

function horizontalRoad (maxX, minX, y) {
	for (let i = minX; i < maxX; i++) {
		city[y][i] = 5;
	}
}

Now onto the main part of our algorithm. This is where the recursive part comes in. So we must remember that in a recursive function there must be at least one recursive call and a base case.

If you remember back to our steps we need to find the length of our x and y. For simplicity let's call them dx and dy.

divide(14, 14, 1, 1) // Note: doesn't include border

function divide (maxX, maxY, minX, minY) {
	//console.log(city);
	let dx = maxX - minX;
	let dy = maxY - minY;

	let split;
	
	if (dx > 4 || dy > 4) {
		if (dx == dy) {
			split = Math.floor((maxX+minX)/2);//Math.floor(Math.random() * dx) + minX;
			verticalRoad(maxY, minY, split);
			
			divide(maxX, maxY, split+1, minY); 
			divide(split-1, maxY, minX, minY);
		}
		if (dx > dy) {
			split = Math.floor(Math.random() * dx) + minX;
			verticalRoad(maxY, minY, split);
			
			divide(maxX, maxY, split+1, minY);
			divide(split-1, maxY, minX, minY);
		} else {
			split = Math.floor(Math.random() * dy) + minY;
			console.log(split)
			horizontalRoad(maxX, minX, split);
			
			divide(maxX, maxY, minX, split+1);
			divide(maxX, split-1, minX, minY);
		}
	} 
}

If you want to learn more about different maze algorithms or read their description of Recursive Division: https://en.wikipedia.org/wiki/Maze_generation_algorithm

Connecting the Map to every script

At the bottom of the map maker script, export the city, cleanUp function, and the divide function for the use of other scripts:

export{city, cleanUp, divide};

Now to access this information: in the map_script access the exports from the map_maker script. Here, the export have been labeled chooseName.

import * as chooseName from '/DrivingSimulator/map_maker.js';

Use the city from map_maker script instead of making another array. Hense chooseName.city...:

function setup () {
	map.innerHTML="";

	for (let i = 0; i < chooseName.city.length; i++) {
		for (let j = 0; j < chooseName.city[0].length; j++) {
			let x = (i * 5) - (2.5 * (chooseName.city.length-1))
			// y is set in each building type
			let z = -j * 5 + (2.5 * (chooseName.city.length-1))
			let val = chooseName.city[i][j];
			
			addbuilding(val, x, z);
	}
}

As we said we would in Section 4, let's export our setup function from map_script just like we did earlier with map_maker:

export {setup};

Lastly, our movement script. it needs to import the map to check for walls and empty spaces in the newly generated maps. As well as the functions we export to change the map.

import * as chooseName from '/DrivingSimulator/map_maker.js';
import * as pickName from '/DrivingSimulator/map_script.js';

Reading the map in the movement_script

var moveMap = chooseName.city;
building_to_value();

function building_to_value() {
	for (let i = 0; i < moveMap.length; i++) {
		for (let j = 0; j < moveMap[0].length; j++) {
			if (i == 0 && j == 7) {
				moveMap[i][j] = 3 // tunnel to next map
			} else if (moveMap[i][j] == 13) {
				moveMap[i][j] = 2;
			} else if (moveMap[i][j] == 0 || moveMap[i][j] == 5.5 || moveMap[i][j] == 5 || moveMap[i][j] == 10) { 
				moveMap[i][j] = 0; // empty
			} else {
				moveMap[i][j] = 1; // building
	}
		}
	}
	
}

Moving from Map to Map

We'll have two methods of moving from map to map: a button, and a zone. Since these to methods do the same thing, lets have a helper function.

restart: function () {
		console.log(moveMap);
		chooseName.cleanUp();
 		chooseName.divide(14, 14, 1, 1);
 		pickName.setup();
 		building_to_value();
 		this.el.object3D.position.set(30, 0.08, 0);
 		this.movePercent.x = 0;
	},

For debug purposes, when you press "O" the map will change.

// In the eventlistener of the init function
if (name == "o") {
 					self.restart();
		 		}

But generally to move from map to map, you have to drive through a tunnel.

if (loc == 3) {this.restart(); }

That is it! Congrats, you've come to the end of this tutorial. You are champion for reading this far! I challenge you to continue with A-Frame, either with this project or one of your own.

Back to Top


Potential Challenges:

Back to Top