Hi folks, I'm Hunor and in this course, we
are going to build a racing game with Three.js. In the game, the player controls the car on
the left track and has to race as many laps as possible without colliding with any other
vehicles. As the game goes on, more and more vehicles
pop up on the right track, and avoiding them becomes trickier. The only thing you can do to avoid a collision
is to accelerate or decelerate the car. You can't fully stop it though. So while it is easy at the beginning it will
get quite tough as more and more vehicles show up on the other track. In this course, we are going through how to
build this game with Three.js. We start with the basics, we set up the scene,
the lights, and the camera. Then we build a minimalistic car by putting
together boxes. We also add texture to it to paint the windows. For the textures, we not just going to import
an image, we are going to code our textures with HTML canvas. Once finishing with the car, we create the
race track. We talk about how to draw two-dimensional
shapes in Three.js and how to turn them into extruded geometries. This part will also contain some trigonometry
as we need to calculate some angles to draw the track. Don't worry though, we are going to cover
everything in detail. And once we know how to create a car and the
track in the 3D space we will add the game logic. We add event handlers and the main animation
loop that will move around the vehicles and take care of the game logic. There's a lot to cover in this course so let's
get right into it. The only thing you need as a prerequisite
is a basic understanding of JavaScript. We won't do anything crazy with JavaScript,
but we won't go through the code line by line either. So let's start with setting up our project. To generate graphics we use Three.js. Three.js is a JavaScript library that uses
WebGL under the hood to generate 3D graphics in the browsers. Now you might be thinking, okay why don't
we just use WebGL then? WebGL is rather a low-level API and you need
to do a lot, even to paint a simple box on the screen. On the other hand Three.js is like playing
with Lego. We just put together a scene with some geometries,
add some light, set up a camera and we have a 3D render. As Three.js is a library we need to add it
to our project first. There are several ways to do this. Probably the easiest is if you are using CodePen.io. I use CodePen for most of my demos because
you always have a live preview while you are coding and you can easily share your work
with others. If you use CodePen, after you create a Pen
you have to go to Settings and under the JS tab, you need to add Three.js as an external
library. If you prefer to code on your local computer
though then you also need to make sure you import Three.js. You can use npm to install Three.js to your
project then import it. Once we added Three.js to our project let's
start by setting up the scene. We are going to set up the lights, the camera,
and a renderer. I added Three.js to my project with node package
manager so as a first step, I import it here. Then first let’s create a Three.js scene. A scene is a container. It will contain all the 3D objects that we
want to display, and the lights. Here we generate a 3D object representing
the player's car with a function that we are going to discuss in a minute. Then we add this car to the scene. As a next step, let's add the lights. We add two lights, an ambient light, and a
directional light. The ambient light is shining from every direction. We use it to have a base color for our objects. We define an ambient light by setting its
color and intensity. The color is usually white. The intensity is a number between 0 and 1. The two lights we define will be used simultaneously
so we want each the intensities to be around 0.5. Once we defined the light we add it to the
scene and we define a directional light. We set up a directional light in a similar
way as the ambient light, by setting a color and an intensity. The directional light will also have a position. And the position here is a bit misleading
because it doesn’t mean that the light is shining from that position. The directional light is like the sun. It shines from very far away with parallel
light rays. With the position, we define the direction
of these light rays. From all the parallel rays we define one. This specific light ray will shine from the
position we define to the 0,0,0 coordinate. The rest will be in parallel. By setting the position of the light we determine
which side of our objects will be the brightest and which ones stay in the dark. Here the light we define is primarily shining
from the top, so the top of the car will be the brightest. The light is also significantly moved along
the Y-axis so the right sight of the car will also receive a good amount of light, but less. And while the light is also moved a bit along
the X-axis, the front of the car won't receive that much light. If we go back to the code, here we define
the light's position by setting its x, y, and z coordinate. We can see that the X value is indeed the
lowest, so that's why the front of the car is the darkest. The Y and Z values are close, still, the Z
position is the highest value, hence the top of the car is the brightest. Now that we have lights, let's set up a camera
to define how do we look at the scene. There are two options here. There are perspective cameras and orthographic
ones. Video games mostly use perspective cameras
because how they work is closer to how we see things in real life. In this game though, we are going to use orthographic
projection. With orthographic projections, things will
have the same size no matter how far away they are from the camera. They also don't distort the geometries. The parallel lines will remain in parallel. This will give our game a more minimal, geometric
look. For both type of cameras, we have to define
a view frustum. This is the region in the 3D space that is
going to be projected to the screen. We are not going to use a perspective camera
in this game, but if you want to experiment with it, this is how you can set it up. On this figure, you can also see how this
projection works. Everything within the view frustum is projected
towards the viewpoint with a straight line. You only need to define four parameters to
define a perspective camera. You need to define a field of view, which
is the vertical angle from the viewpoint. Then you define an aspect ratio of the width
and the height of the frame. Then the last two parameters define how far
the near and far planes are from the viewpoint. Things that are too close to the camera will
be ignored, and things that are too far away will be ignored as well. Of course, normally the near plane is closer
to the camera because you don't want to miss things right in front of you. This is just an example. Then there’s orthographic camera that we
are actually going to use. Here we are not projecting things towards
a single point but towards a surface. Each projection line is in parallel. That’s why it doesn’t matter how far objects
are from the camera, and that’s why it doesn’t distort the geometries. For an orthographic camera, we have to define
how far each plane is from the viewpoint. We define that the left plane is 75 units
away to the left, the right plane is 75 units away to the right, and so on. Here these units don't represent screen pixels. There's going to be a later setting where
we display the rendered scene in the browser. Here these values have an arbitrary unit that
we use in the 3D space. Later on, when defining 3D objects in this
space we are going to use the same unit to set their size and position. Regardless of what camera are we using, we
also need to position it and turn it in a direction. Here we are moving the camera by 200 units
along the X and Y axis and move it along the Z-axis by 300 units. Then we set which direction is upwards. We set that the Z-axis should point upwards. This is not the default, by default the Y-axis
points upwards. And finally, we set that the camera should
be looking towards the 0,0,0 coordinate. This way through this camera we can see three
sides of our car. The final piece we need for set up is a renderer
that renders the scene into our browser. We define a WebGLRenderer. This is the piece that renders our scene into
an image according to our camera then displays it in our HTML. Here we also set up the actual size of the
canvas. We want to fill the whole browser window so
we pass on the window's size. And finally, the last line here adds this
rendered image to our HTML document. It will use an HTML Canvas element to display
the image. As a next step, let's see how can we compose
a car. In the first round, we will create a car without
textures. It is going to be a minimalistic design, we
just put together four boxes. We define the Car function we used before
to return our 3D object. This function starts with creating a Three.js
Group and ends with returning it. This group is another container like the Scene. It can hold Three.js objects. It is convenient because when we will move
the car we will simply move around the Group and we don't need to move each individual
piece of the car. First, we create the back wheels. We will define a gray box that will represent
both back wheels. As we never see the car from below, the player
will never notice that instead of having two back wheels we only have one big box. We define the back wheels as a mesh. The mesh is a combination of a geometry and
a material and it represents the 3D object. The geometry defines the shape of our object. In this case, we create a box by settings
its dimensions along the X, Y, and Z-axis to be 12, 33, and 12 units. Then we set a material that will define the
appearance of our mesh. There are different material options. The main difference between them is how do
they react to light. We use MeshLambertMaterial. The MeshLambertMaterial calculates the color
for each vertex. In the case of the drawing a box that's basically
each side. We add this box to the scene by adding it
to the group, and adding the whole group to the scene. By default the box will be in the middle,
its center will be at the 0,0,0 coordinate. Let's move it into position. First, we raise it by half of its height. So instead of sinking in halfway to the ground,
it will lay on the ground. Then we move it back along the X-axis to reach
its final position. In the same way, we can define the front wheels
as well. It's exactly the same geometry and material. The only difference is that we move this one
in the other direction along the X-axis. Then we define the main part of the car. The geometry will be another box with different
dimensions. This mesh we raise above the ground by setting
its Z position higher than half of its height. Then the same way we add the top of the cabin
as another mesh. Before moving on with the textures let's adjust
a few things. You can see that the back wheels and the front
wheels are ultimately the same mesh. Instead of defining them twice, we can create
another function that crates this mesh. This will also come in handy if we define
other vehicle types. If we define a truck for instance we can use
the same wheels there as well. We could optimize this even more. We recreate the same box geometries and materials
for each wheel. We could create one global wheel geometry
and wheel material, then reuse them each time we create a new wheel. Here we keep it simple though, for a minimal
game like this it doesn't make that much of a difference. As a next step, we can also randomize the
color of the cars. On the top, we have an array of possible color
codes for the vehicles as hex values. On the bottom, we define a utility function
that picks a random value of a given array. Let's use these when setting the color of
the main part. We call this utility function to pick a color
from our colors array, and that will be our car's color. This way when creating a new car it can end
up being red, yellow, or green, or any other color you list in this array. Now that we have our very basic car model,
let's add some textures to the cabin. We are going to paint the windows. We will define a texture for the sides and
one for the front and the back of the cabin. When we set up the appearance of a mesh with
a material, setting a color is not the only option. We can also map a texture. We can provide the same texture for every
side or we can provide materials for each side in an array. We are going to do the second to have different
textures on the different sides. As you can see some textures won't fit right
away. Three.js doesn't know how we want them so
we will need to adjust them by rotating or mirroring them till they reach their final
position. To have a texture we could use an image. But instead of that, we are going to to do
the extra mile and create textures with JavaScript. We are going to code images with HTML Canvas
and JavaScript. Before we continue, we need to make some distinctions
between Three.js and HTML Canvas because otherwise, things might get a bit confusing. Three.js is a JavaScript library. It uses WebGL under the hood to render 3D
objects into an image and it displays the final result in a canvas element. HTML Canvas on the other hand is an HTML element,
just like the div element or the paragraph tag. What makes it special though is that we can
draw shapes on this element with JavaScript. This is how Three.js renders the scene in
the browser. And this is how we are going to create our
textures. So let's see how we can do that. As we are going to draw on canvas, first we
need to create a canvas element. While here we create an HTML element, this
element will never be part of our HTML structure. On its own, it won't be displayed on the page. Instead, we will turn it into a Three.js texture. Let's see how can we draw on this canvas. First, we define the width and height of the
canvas. This size has nothing to do with how big the
canvas will appear, it's more like the resolution of the texture. Once we defined the size we get the 2D drawing
context. We can use this context to execute drawing
commands. First, we are going to fill the whole canvas
with a white rectangle. To do so first we set the fill style to be
while. Then fill a rectangle by settings its top-left
position and its size. When drawing on a canvas, by default the 0,0
coordinate will be at the top-left corner. Then we fill another rectangle with a gray
color. This one starts at the 8,8 coordinate and
it doesn't fill the canvas it only paints the window. And that's it, we draw two rectangles with
JavaScript to an HTML Canvas. The last line turns the canvas element into
a texture and returns it, so we can use it for our car. In a similar way, we can define the side texture. We create a canvas element again, we get its
context and then first fill the whole canvas to have a base color, then draw the windows
as rectangles. Of course, these two windows have different
starting positions and different sizes, yet the idea is the same. Now let's see how can we map these textures
to our car. When we define the mesh for the top of the
cabin, instead of setting only one material, we set one per each side. We define an array of six materials. We map textures to the sides of the cabin,
while the top and bottom will still just have a plain color. If we map these textures just like that, then
some of them, like the front texture, won't look like how we want it to be. To fix the front texture, we have to set a
rotation and turn it by 90 degrees. We have to set this rotation in radians, so
we set it to half PI. Before turning it though, we have to make
sure that the texture is rotated around its center. This is not the default. We have to set that the center of rotation
is halfway both vertically and horizontally. We set 0.5 on both axes, which is basically
50%. Now, these two are good. If we turn around the car though, we'll see
that the two other textures are a bit off as well. Let's start with the back texture. We do the same thing as we did before, except
we turn this one the other way. Then finally we need to flip the left side
to fix it. We flip it along the Y-axis to have it in
the correct position. And that's how we set up the textures. We draw on canvas elements with JavaScript,
turn them into textures, and map them. The same way as we created the car we can
create a truck or a tree. We are not going to get to the details here,
because it's ultimately the same thing. The truck is also a group of boxes. It can even use the same Wheel function that
we created for the car. The only difference here is that the cabin's
material receives both a color and a texture, and Three.js will mix the two together. For the trees, we use both a sphere and a
box geometry. To have a bit of diversity, we can even generate
threes with different sizes. I let you figure these out as an exercise. And instead, let's move on with something
more interesting, the race track. Let's see how to set up our race track. Our race track will have two layers. The bottom layer is a plane geometry. This is just a flat surface and it will be
ultimately the track itself. We will add some texture to it for the line-markings. Then we add three islands and the surrounding
field. We will define these as 2D shapes then we
turn them into an extruded geometry. An extruded geometry basically turns 2D shapes
into 3D objects by giving them a depth. They will not just float above the ground,
but they stick out of it. Now let's see how do we build up these shapes. If we look at the track from above, we will
see that under the hood we only have a few circles. And to draw these islands we need to draw
some arcs. Let's walk through how can we draw an arc,
and what parameters do we need to draw one. Let's take this one. First, we need to define its center position. This will be somewhere on the X-axis. Then we need to set a radius for this circle
and we have to say from which angle does the arc start and where does it end. The arcs are symmetric to the X-axis so the
start and end angles have the same value, with opposite signs. To define this arc we can just make up an
angle. Let's say it starts at 60 degrees and ends
at -60 degrees. But then when we draw this other arc, we need
to know where exactly should it start and end to match the previous one. We need them to look connected so that they
form a shape together. This second arc has a different center position,
a different radius, and a different angle as well. We need to calculate these values for a precise
match. In a similar way, we need to calculate the
angle for the small island in between, and the angle for the outer arcs. So before drawing our track, let's calculate
these values. To calculate the values we need, we are going
to use trigonometry. So let's have a quick review of how the sine
and cosine functions work. Trigonometry says that in a right triangle
the sine value of an angle equals the opposite side's length divided by the hypotenuse. In a similar way, the cosine value of an angle
equals the adjacent side divided by the hypotenuse. In other words, we can say that if we multiply
the sine value of an angle with the hypotenuse then we get the opposite side's length. Or the cosine value of an angle multiplied
by the hypotenuse equals the adjacent side. In our examples, the hypotenuse will always
be the radius of a circle, and we are going to use trigonometry to get the position of
a point on the circle. Trigonometric functions also have reverse
functions. We can calculate an angle in a right triangle
if we know the opposite side and the hypotenuse, or the adjacent side and the hypotenuse. We are going to use these functions as well. Now let's see how can we make use of these
functions to get the angles and positions we need to draw our track. First, we define a track radius. This will be the distance from the center
of a track to the line-markings. Then we define a track width. From these two, we can calculate the inner
track radius and the outer track radius. If we want to compare the sizes defined here,
with the car we defined earlier, then this is how the car will look like on the track. Now let's calculate the angles we need for
drawing the islands. We define the first angle to be 60 degrees. We have to define the angles in radian though
so we set it as 1/3 PI. Then we calculate a value we need temporarily
to get the second angle. This value gives us how far away the starting
point of this arc is from the X-axis. This distance is the opposite side of a right
triangle, so we can multiply the sine value of our angle with the inner track radius to
get this size. Now that we know this value, we can calculate
the second angle. This angle should point to the same position. Here we also have a right triangle and one
of the sides is the same as the one we just calculated. We can use trigonometry again to get this
angle. Now we do the reverse calculation so we use
arcsine. And the radius is also different now, so we
use the outer track radius in the calculation. Now we know these two angles. It's time to calculate the center position
of these arcs. So far we didn't actually know how to place
them or what's the distance between the two. We use trigonometry again to get the distance
of the two centers. First, we get the horizontal distance from
the center of the left arc to the point where the arc starts. We want to get the adjacent side in a right
triangle so now we use the cosine function. And then we do the same thing with the other
side. The other side of course has a different angle
and a different radius. If we add these two values together we get
the distance of the two centers. And if we divide this value by two, then we
get the distance of each circle from the center of our coordinate system. I call this value arcCenterX. One arc center will be at the -arcCenterX,0
position and the other will be at +arcCenterX,0. Knowing the center position of these arcs
and the first two angles we can draw the left and right islands. To draw the middle island and the outer field
we still need to calculate two other angles. To get the angle for the middle island we
use the inner track radius and the arcCenterX we just calculated. With these values, we can use arccosine to
get the angle. In a similar way, we can use the outer track
radius and arcCenterX to get the angle we use for the outer field. Now we have all the angles and positions we
need. We can start drawing our track. Before we draw the field, first let's make
sure that the camera is at the right angle and the whole field will fit inside the scene. At the beginning where we defined the camera,
we had a camera width setting. We set this value to fit the car inside the
picture, and now we have to fit the whole track. So we change this from 150 units to 960. The second thing we want to change is that
so far we were looking at the car from the side, and now as we are creating the track
we want to see it from above. We change the camera position. And let's also remove the setting that tells
for which way is up. As we are looking downwards now, this setting
would only confuse the camera. And finally, let's call a function that we
are just about to define. The function that draws the map. This function receives two parameters. The width and height defining the size of
the map. The width will be the same as the camera width,
as horizontally the track will fill the whole image. And of course, we want to fill the whole image
vertically as well, but to achieve this the height has to be bigger than the camera height. If we would only look at the track from above
then the same height would be fine. But later we are going to move the camera
along the Y-axis to see the track a bit from the side. And then with the rotating camera, this value
needs to be higher because otherwise, you would see the edges of the map. Then let's see how do we define the renderMap
function. The renderMap function adds two things to
the scene. A gray plane with the line-markings and a
few islands with a green field that stands out of it. First, we create the plane. This will be a 2D surface that will be our
foundation. It's another mesh consisting of a geometry
and a material. The plane geometry takes the width and height
that the renderMap function received as parameters. At first, we can define the material with
a plain gray color. Once we created the mesh we add it directly
to the scene. Then instead of defining its material with
a plain color, we can also set up a texture for it. Let's see how this texture is created. We create a texture the same way as we did
before for the car. Except for this time, we are not just filling
rectangles. We are going to draw arcs. We create a canvas element again, draw to
its context, and in the last line, we return the canvas element as a texture. It's important that we set the size of the
canvas to be the same as the width and height of the plane. This is because we want to be able to use
the same units we use in Three.js when drawing on the canvas. First, we start as before and we fill the
whole canvas with a plain color. Then we set things up for the first circle. We are going to draw the circle as a stroke. We set the stroke to be 2 units wide and we
set a color for it. Then we also set a dash array. This array defines that after a 10 unit stroke,
there will be a 14 unit gap. Once we set these things up we can draw our
circles. Unfortunately in HTML canvas, there's no command
to draw a circle. Instead, we have to draw an arc. We already talked about how to define an arc. We have to set its center position, its radius,
and its starting and ending angle. Here because we are drawing a full circle
we start at 0 degrees and finish at 360 degrees, or in radians, we go from 0 to 2 PIs. First, we set the position using the arcCenterX
value we calculated earlier. You might note that we don't simply set the
center to be at the -arcCenterX,0 position, we also add half of the map size. This is because even though in Three.js the
center of the coordinate system is in the middle, in HTML canvas it always starts at
the top left corner of the canvas. So first we calculate the center of the canvas
then go from there. You might also note that the arc command is
not a standalone command. The arc has to be part of a path, even if
it is the only one. Whenever we draw a path in HTML canvas we
always start with beginPath, then we have one or more commands, then we finish with
the stroke or fill command. Here we don't want to fill this shape, just
get the border of it, so we finish with the stroke command. In the same way, we draw the other circle. The only difference here is the position. Here you might be wondering why don't we just
use one path instead of two. If we would define one path with two arcs,
then HTML Canvas would try to connect them to have a continuous stroke. We don't want that. We want two separate circles, so we define
two separate paths. Now back in the renderMap function we see
that we use our new texture, and we see the result on the right. Let's move on with drawing our islands. We are going to draw our islands as 2D shapes,
then we turn them into an extruded geometry. The extruded geometry is just another geometry
like the box or the plane geometry we just used. As a parameter though it takes one or more
2D shapes and an options object. In this options object what we primarily want
to set is the depth of the geometry. In our case, how much should these islands
stand out from the ground. The second option is setting if we want to
use a bevel. The default is true, and that would make the
edges rounded. We don't want that, so we set this value to
false. And once we create this geometry we define
a mesh with it, that we can add to the scene. This mesh will have two materials. One for the top, and one for the sides. We want to have a different color on the top
of these islands and on their side. Now let's see how do we create this 2D island
that we turn into an extruded geometry. Let's check out the getLeftIsland function. The getLeftIsland function will return a Three.js
Shape. And this is where things will get a bit confusing
again, as a Three.js Shape works in a very similar way as HTML Canvas. We have similar commands with similar parameters. Here we draw an arc again, and we draw an
arc by providing the same parameters. We set the center position, the radius, the
angles, and if the arc should go clockwise or not. But there are differences as well. First of all the function name is absarc,
which means that the arc is absolutely positioned. Earlier in HTML canvas we also used absolute
positioning, just the function name is different. Then the positioning is also different. Earlier we positioned the shapes relative
to the top left corner of the canvas. With Three.js shapes, we don't have a top-left
corner. We place things in the infinite space and
the 0,0 coordinate is right in the middle of the screen. So we can just say that the center of the
arc is at -arcCenterX, 0 position. Then we set the radius and the angles. The starting angle is arcAngle1, and the ending
angle is its opposite. We also state that this arc goes counterclockwise. You might also note that unlike with HTML
Canvas, this arc doesn't start with a beginPath command, and it doesn't end with stroke or
fill command either. This is because the whole Three.js Shape is
a path itself, and the two arcs we define here together will define the shape. So let's see the other arc. The other arc has a different center position,
a different radius, this one uses the outer track radius, and it has another angle. It doesn't just use the angle we calculated
earlier. The angles always start from the right side,
and this arc is in the opposite direction. So first, we do a half-turn and then turn
by arcAngle2. It's also important to note that this arc
is going clockwise. The two arcs we define here need to define
a continuous path because otherwise, they wouldn't end up in a shape. The same way we define the getLeftIsland function,
we can define a 2D shape for the island in the middle. First, we draw the arc from the left and then
we continue it with the arc from the right. Both of this arcs use the inner track radius,
both use arcAngle3 which we calculated specifically for this island and they both go clockwise. Then we can create a 2D shape for the right
island as well. This is literarily the same shape as the left
one, except it is mirrored so some values have the opposite sign. Then we draw the 2D shape of the outer field. This function receives the size of the map
because we want to fill the whole map. We start this one from the outside. With the first command, we move to the bottom
left corner of our map. If you are using subtitles they might overlap. And as the map's height is twice the camera
height it should be actually outside of the visible area. I'm cheating a bit here. Then from the bottom left corner, we draw
a straight line to the middle of the bottom edge. Drawing commands always continue the previous
command. Both here when we define a Three.js Shape
and in HTML canvas. The lineTo command always draws a straight
line from the point where the last drawing command has ended. Then we draw the left outer arc. This is similar to the arcs we draw before. It uses the outer track radius, arcAngle4
and it goes clockwise. And while it is not connected with the previous
path segment directly, it is implied that it continues the previous command. We still have a continuous path. Then we draw the other arc. It also goes clockwise in order to continue
the path. Then we go down to the bottom edge with a
straight line and then we go around the edges of the map. We go to the right first, then up, then to
the left. And then again, we don't need to close the
shape, it is implied that the shape will close itself because otherwise, it would be just
a path and not a shape. Now back in the renderMap function now all
our islands and the outer field are passed on to the extruded geometry. They are all part of our field mesh. And this is how we create the track. Now if we move the camera position along the
Y-axis to have a look at the scene from an angle then we see that the extruded geometry
stands out from the ground. Feel free to decorate the track with some
other objects. Add trees or buildings around the track. You can create a nice map even with some basic
boxes and other geometries. Now that we know how to create a car and we
drew our race track, it's time to finally add the game logic. Here's an overview of the main parts of the
game that we are about to cover. As we saw we have a playerCar variable that
is global and it represents the Three.js mesh of the player's car. This is the car that the player can control,
by accelerating or decelerating it. We are also going to have an array of the
other vehicles. These are cars or trucks that move on the
other track. To populate this array we are going to have
an addVehicle function that generates a random car or truck with its own color, speed, and
direction. Then we are going to have the main animation
loop. This keeps everything moving and handles the
game logic like adding new vehicles and calling hit detection. This will keep track of some global variables
like playerAngleMoved and score. We also have event handling that accelerates
and decelerates the player. And finally, we have a reset function that
sets everything back to the initial, and a start function that will kick off the animation. So let's go over all of these in detail. We start with reset. Even though it might sound like this is the
last thing we need to call, we also use it to initialize things. This code snippet here is after we initialized
the scene, we added the player's car and the track and we initialized the camera and the
renderer. We start with defining some globals. We define a ready variable. That is a boolean and its value is true if
we can start the game. This variable will help us not to trigger
the animation loop more than once. We define the playerAngleMoved which we will
see just in a second, and the score variable along with a reference to the score element. The latter points to an HTML element in your
HTML structure. We don't cover the HTML structure here, because
it mainly just consists of the canvas element that Three.js generates. To turn this into a real game you might want
to add a result screen and instructions via HTML. And a score indicator. If you want this line to work, you need to
have an HTML tag somewhere in your HTML with the ID "score". Then we also have the other vehicles array
and the lastTimestamp that we use in animation. Then we call our reset function to initialize
these values. The reset function serves both as a reset
and for initialization. The only interesting part here is that when
we reset the otherVehicles array, we don't just set it to an empty array. We also have to remove the vehicles from the
scene. We also reset the player position by calling
a function we are going to discuss later. This will move the player's car to the starting
position. And as the last step, we render the current
state of the scene. Remember this piece of code is after the lines
where we added the player's car and the track to the scene, and we initialized the renderer. And in the last line, we state that we are
ready to start the game. Then we add event handlers. We want to listen to the key down and key
up events, specifically when the player hits the up or the down keys. These event listeners will primarily change
two other globals, the accelerate and decelerate variables. Once the up arrow is held down the accelerate
variable should be true, and once the down arrow is held down the decelerate variable
should be true. We also have a third case here, pressing the
R key will reset the game by calling the reset function we just defined. Pressing down the up arrow will also start
the game by calling the start game function. This will check if the game is in a ready
state and if it is then starts the animation loop. The primary purpose of this ready variable
is to avoid triggering the animation loop more than once. Every time the player presses the up kep,
the start game function will be called. But only the first time will it trigger the
animation loop. The animation loop is triggered by the renderer’s
setAnimationLoop function. This works in a very similar way as requestAnimationFrame
works. We pass on a callback that takes care of the
animation logic. The main difference is, that the requestAnimationFrame
only runs once and after every animation frame, you have to trigger the next one. With setAnimationLoop this is automatic, and
you have to specifically tell the animation to stop. So let's see how we create the animation loop. The animation function will move everything
that needs to move and take care of the game logic. It will add new vehicles to the track when
needed, update the score and even call hit detection. It receives a timestamp. This timestamp is constantly increasing as
the game goes. We could better use the time difference between
two animation frames, so we calculate that. We keep track of the previous animation frame's
timestamp in a variable called lastTimestamp. And in every turn, we subtract this value
from the current timestamp to get the time passed between the two animation cycles. We call the time passed between two animation
frames timeDelta. At the end of every animation frame, we render
the modified scene. Let's see what happens before that. First, we move the player's car. We call the movePlayerCar function and we
pass on the time passed. Let's see how it works. The player's car should move around the track
in a circle. Instead of defining the car's position by
an X and Y coordinate, we define it with an angle. This angle will tell where should the car
be around the circle, and from this, we will calculate the X and Y position. This angle has two components. The starting position and the actual movement
the car made. We separate these two because when we calculate
how many laps the player took, we will only use the actual movement and not the initial
position. The player starts at 180 degrees, which is
PI in radians. This initial position we call playerAngleInitial. This is a constant value. And as the game goes we increase, or as the
car goes clockwise actually we decrease the playerAngleMoved variable. These two angles make up the totalPlayerAngle,
which we will use to calculate the car's position. But first, let's see how the playerAngleMoved
variable is changing. In every animation loop, we calculate by how
much should this angle change. First, we calculate the playerSpeed, which
will give us the desired rotation per millisecond. Then we multiply it by the time passed to
get by how much should the car move in this animation frame. This is a delta value that we subtract from
the current angle. To get the player's speed we define another
utility function. This function will give us a speed depending
on the accelerate and decelerate globals. If the player is accelerating it gives back
double the base speed, and if it's decelerating it only gives back half of it. And if no acceleration or deceleration is
happening, then it gives back the base speed. This base speed we define as a constant. It has to be a very small number. It represents how much the angle changes every
millisecond, if no acceleration or deceleration is happening. Now let's calculate from this angle how do
we get the X and Y position of the car. We use trigonometry again. We can draw a right triangle between the center
of the track and the car. Then we can calculate the horizontal component
of this triangle using the cosine function. Then to get the X position of the car we shift
this by the circle's position. In a similar way, we calculate the vertical
component of this triangle with the sine function. We can calculate the X and Y position this
way for any given angle. On every animation frame we change the angle,
then recalculate the actual position with trigonometry. The last thing we need to calculate here is
the car's angle. The car is also rotating as it goes around
the track. This rotation should always be the main angle
minus 90 degrees. Now that we placed the player's car, let's
go back to the animation function and see what else do we have. Once the player moved we can recalculate if
it made another lap. We calculate the number of laps by dividing
the moved angle with a full turn, that is 360 degrees. As we use radians, we divide by two PI. We also use Math.floor here, because we also
want to have a whole number, we don't want something like 1.5 laps. Then we check if the number of laps equals
the score number and if not then we change it. We have this condition here because we don't
want to update the HTML element representing the score with every animation frame. If it didn't change then we don't update the
DOM. The main animation loop also takes care of
adding other vehicles. When the game starts and with every five laps
we add a new vehicle to the other track. We check how many other vehicles we already
have and if we need more, then we add one. Let's see how the addVehicle function works. The addVehicle function will add a new car
or truck both to the scene and to the otherVehicles array. It picks a random vehicle type, a car or a
truck then creates a mesh for it. In this course, we didn't cover how to do
a truck, because you can do it in a very similar way as the car. I let you figure out how to create a truck
yourself. Or if you want you can add even more vehicle
types to this game. We add this mesh to the scene first then we
set up a few other parameters for this vehicle. First, we need to decide if the vehicle will
go clockwise or counterclockwise. We do this by generating a random number between
0 and 1 and check if it's more than half. Then we decide on the starting position. If it goes clockwise, then the vehicle starts
at the top of the track at 90 degrees, at half PI in radians. If it goes counterclockwise, then it starts
at the bottom of the track, at minus half PI. We do this because we want to give the player
some time before it potentially crashes into this new car that just popped up out of nowhere. Then we need to figure out the speed multiplier. This is a multiplier because we are going
to multiply the base speed with this value. So if a speed multiplier of a vehicle is one,
then that vehicle will go with the same speed as the player by default. The speed multiplier will be a random number
in a range, depending on the vehicle type. In the case of a car, it will be somewhere
between 1 and 2. As a last step we add all these, the 3D mesh,
the direction, position, and speed to the otherVehicles array for later access. We are about to need all these when moving
around the vehicles. Let's see how that goes. Back in the animation function, the next thing
we are about to do, is to animate the other vehicles. This function also receives the timeDelta
variable, because the movement depends on the time passed. In this function, we do a very very similar
thing as we did before when moving around the player's car. We go through the otherVehicles array, and
based on their direction and speed we change their angle. Then we use this new angle to calculate the
X and Y position of the vehicle along with their rotation and place them accordingly. Now back in the animation function, there
is a last thing we need to do. Hit detection. Once both the player and every other vehicle's
position has been updated we need to make sure that the player didn't crash into any
other vehicles. The other cars don't crash, they can go through
each other. The player though has to avoid collision. Let's see this scenario. The player is entering the intersection, while
another vehicle is already there. How do we know if they crashed? We are going to calculate hit zones. Each vehicle has two of them and if they overlap
then we have a crash. This is not necessarily the most precise calculation. It can happen that the cars do not hit each
other yet, but the hit detection already indicates a crash because of the hit-zones overlap. The hit detection we are about to cover here
is a rather basic one. You can always create a more precise one,
or even use another third-party library to calculate collision. We are about to see that we don't need to
calculate circles here. We only need to figure out the center of these
zones, then we can see if they are too close to each other or not. So let's calculate the center position of
these hit zones with a new utility function. We pass on to this function a few things. We pass on the player car's position. Then we also pass on the player's angle and
the fact that it goes clockwise. The last thing we pass on is how far away
should the center of the hit-zone be comparing to the car's position. From all these values our utility function
will give us the X and Y coordinate of the center of the hit-zone. It uses trigonometry again to get the horizontal
and the vertical distance from the player's position. Now the same way we can get the center of
the other hit zone. We pass on the same parameters, except this
one is shifted backward. Then we go through all the other vehicles. We go through the otherVehicles array with
the some function. This function will give back true in case
any of the vehicles have a hit. We can define different hit zones and hit
detection for different vehicle types. A truck for instance might be longer, so we
can even define three hit-zones. We only cover the cars here now. We define hit zones the same way as we define
for the player car. We pass on the other vehicles' position, direction,
and so on. Then once we get the center position of these
hit-zones we calculate the distance between the player's hit zones and the other vehicle's
hit zones. We use another utility function that gives
back the distance between two points. It uses the Pythagorean theorem. That says the distance between two points
is the square root of the sum of the vertical and horizontal distance's square. We have a hit if the frontal hit-zone of the
player's car is too close to any of the other vehicle's hit-zones or when the front of another
vehicle is too close to the back of the player's car. We don't cover the hit detection with a truck,
because we didn't cover how to draw the truck either. You can define it in a similar way. If for any of the vehicles one of the hit-zones
is too close, our hit variable will be true. In this case, we can stop the game. We can stop the animation loop by passing
on null to the setAnimationLoop function. This will stop everything. And because the game is not in the ready state,
the player won't restart the animation loop accidentally by pressing the up key. This is also the right place to add some other
logic. Like showing up a result screen with the final
score and telling how to restart the game. Which we can already do by the way. If you remember, in the event handlers we
defined a case for resetting the game. If you hit the R key it will call the reset
function, which resets all the variables we changed and removes every other vehicle from
the scene. It also resets the ready indicator. So next time the player hits the up key, it
will start another game. If we did everything right then we finally
have a game. It was a long course, and I know some parts
were rather complicated. So if you want to go through the code in mode
detail, then you can find the whole source code on CodePen. The link is below in the description. If you want to play around with the code you
find a Fork button at the bottom right corner that will make a copy for you. That version you can change as you like and
your changes will be saved to your account. Thank you for watching. If you liked this course please subscribe. I have some other Three.js game ideas I’m
planning to cover and I’m also uploading web development-related courses on other topics. Let me know what you would like to learn next
or if you have any feedback for this course. Also, don’t forget to check out my earlier
courses on game development. See you at the next one.
Hey that’s really cool. Thanks
Actually it was really great.
Clear voice, accent and unique tutorial.
Looks fun. I'll have to try this out.
Another great tutorial, well done
Well made. Thanks :-)