Three.js Game Tutorial: Learn Three.js while building a traffic run game

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Hey that’s really cool. Thanks

👍︎︎ 2 👤︎︎ u/ElCapitanMarklar 📅︎︎ Mar 17 2021 🗫︎ replies

Actually it was really great.

Clear voice, accent and unique tutorial.

👍︎︎ 2 👤︎︎ u/[deleted] 📅︎︎ Mar 17 2021 🗫︎ replies

Looks fun. I'll have to try this out.

👍︎︎ 2 👤︎︎ u/UnstoppableAwesome 📅︎︎ Mar 17 2021 🗫︎ replies

Another great tutorial, well done

👍︎︎ 2 👤︎︎ u/frank0117 📅︎︎ Mar 19 2021 🗫︎ replies

Well made. Thanks :-)

👍︎︎ 2 👤︎︎ u/gniziemazity 📅︎︎ Mar 29 2021 🗫︎ replies
Captions
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.
Info
Channel: Hunor Márton Borbély
Views: 21,808
Rating: 4.9828572 out of 5
Keywords: threejs, three.js, threejs tutorial, three.js tutorial, three js, three js tutorial, threejs tutorial 2021, threejs course, three.js course, threejs example, threejs for beginners, threejs beginner tutorial, threejs getting started, three.js projects, threejs game, three.js game, three.js game tutorial, threejs game tutorial, game development for beginners, three js 3d game tutorial, three js game, three.js tutorial for beginners, javascript game, html canvas
Id: JhgBwJn1bQw
Channel Id: undefined
Length: 58min 8sec (3488 seconds)
Published: Wed Mar 17 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.