How to make a FPS Player Controller in Godot 3

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Did you always dreamed of making a multiplayer FPS game? Today's your lucky day, because we'll learn how to make these dreams come true. Hey guys, Lucas here and Welcome to Busy Weasel Games. I hope you're all doing great. And today we'll start a new series on how to make a multiplayer first-person shooter game using Godot engine. In this first part, we'll create a project, a map and the basic movement of our player. So without any further ado, let's begin. Open Godot to see the project manager here, we can create and open projects Click on new projects and then type the name you want. I will name it FPS tutorial. Now we need to choose a folder to save our project in. Select one, by clicking on browse, and then choose the folder you want. Now click on create folder and Godot will create a new folder for our project. Just make sure you have open GL3 checked and finally, click on create and edit to open up our brand new project. Before we begin coding. Let me explain briefly, one of Godot's main features: Scenes and nodes. If you already know what they are, feel free to jump ahead to the next part, using the timestamps below. To explain better. What scenes and nodes are. Let me show you an example. Let's make a player, okay? What do we need to make a player in a game? We need a model. Then we need something to collide with the world. And we also need a camera, so we can see what's going on. Looking at this. We can tell that there is a well-defined structure formed by different nodes. And while we can define each individual part as a node. The whole structure is a scene. Basically, a node is an object that is responsible for one, or just a few things. The model, for example, is only the players's visual representation, and the collider only exists to provide collision information. And as you may have guessed, the only purpose of the camera is to show us what's on the screen. The root node player is also a node, but it's parenting every other node in the scene. With that, we can tell that a scene is a collective of nodes. We can build many different things by grouping different nodes together, and we can even put a scene inside another scene. Take our player here. We can add a weapon to it, but the weapon is responsible for too many things to be a node, as it has its own visual representation a magazine and can shoot bullets. So we can break the weapon apart in different nodes. Therefore making it a scene too. See? The system is really powerful and adds a lot of flexibility. If you're struggling a bit here, don't worry. This concept will become crystal clear as we go further in this series. But that's enough of theory for now. Let's make something happen. The first thing we will create is a map. After all, our player needs somewhere to live. What do we need to create? You guess it right. We need to create a new scene. And since we are creating a 3D game, click on 3D scene. Nothing has changed much yet, but that's okay. First off, rename this node here to map. With capital M. And by the way this is a very basic note called Spatial node. That handles some basic stuff in the 3D world and is an excellent option to our map's root node. But how about the ground? Let's create a new node for it. To do this, you have two options. Either you click here, on this plus icon or press Ctrl+A or Cmd+A and the create new node window will open. Well, that's nice, but which node should I use to create the ground? Since we're at the beginning of our game development, we will focus on prototyping everything first and Godot has a nice feature to help hasten this process a bit. If you type in the search field CSG, you'll find these nodes here. They are primitives that we can simply put on our map, fast and easy. They have nice, more advanced features too. But we talk about them later for now. Just choose CSG box and click create. We have a nice little cube in our map. When you're using CSG meshes, you can change their dimensions by clicking and dragging these orange dots here. Like this. Alternatively, we can type the dimensions we want directly in the inspector. Let's make our ground 30 units in width, one unit in height and 30 units in depth as well. To get a better view of the scene. We need to know how to navigate the 3D view. First let's zoom out a bit. Scroll down the middle mouse button to zoom out. That's better. To rotate around the selected object, click and hold the middle mouse button and move it. If you hold Shift, while the holding the middle button, you will pan the view. Like this. By holding Ctrl or Cmd, you can zoom in and out as well, but more precisely. Finally, if you click and hold the right mouse button, you will rotate the view, but around the point, your camera is. Like this. Nice, but we have one problem. If you look closely, our ground is a bit above the zero line on the world. To make things easier. Let's click on this magnet icon to activate snap, and then transform, configure snap. Just type 0.5 in the first field and hit okay. What this will do is to snap everything. We move in half units increments. Now we just need to move down a bit and our ground is now precisely aligned with the world. We already have a ground for our player, but I say we also need two more things, so we can test everything properly. A box and some kind of slope. The box will help us to measure our player's jump height and the slope. Well, moving on slope is a tricky thing. So we'll create one too. First, select the map node, as we want to create a node child of it, and click on this plus icon here, or press Ctrl+A or Cmd+A to open up this window. Create another CSG box node. Cool. We want to use this box to measure the player's jump height, right? But what's the height we want? The right answer to this question will depend on a lot of factors, but since I know that our player's height will be 2.5 units, I've decided it's jump height should be three units. So, in inspector up here type in three in width, height, and depth. Done. Move the box up a bit and position it as you desire. Good. Now we can move on to the slope. Again, select the map's root node and let's create a new node. Type CSG again, but now we'll choose the CSG polygon node. It looks like the same CSG box as before. But if you'll notice the orange circles are placed differently. To create the slope simply click and drag this bottom circle to here, like this, and this one to the same place as the last one. Now, take the circle up and here should be fine. But we need to make the slope thicker. To do this, go to the inspector, here in depth type three. Nice. Position it whatever you fancy, and done. We now have a nice little map with almost everything we need to play our game. Cool. We have a ground, a box, and a slope, but they look pretty ugly, don't they? We can make them prettier by adding materials to them. Let's do this. I've already downloaded some textures from the excellent Kenny's prototype texture pack. You can find the link to these textures in the description. After you've downloaded the textures, choose three of them. All PNG files. Now to go to the project folder, you can easily do this by right clicking on the res folder here and selecting open in file manager. Now let's make our project well structured. Create a new folder and name it, assets, all small caps, and then inside it, create another new folder and name it. textures, all small caps again, Just paste the three chosen textures in here, and we're good to go. You may need to rename some of them though. With all this taken care of, we can now select our ground. You can do it either by clicking on it here in the viewport, or by clicking on our ground node here, let's change some names, by the way. Double click, slope. Right-click and rename box, and finally press F2, ground to make it prettier go to the inspector. Here in material, new spatial material, click on it again, and you should see this panel. There is a lot of parameters that you can change here, but we'll focus solely on changing the texture. To do this, go to Albedo and then click and drag the texture you want on our ground to the texture slot here. This already looks nice, but we can make it even better by going to UV1, setting triplanar to on, and changing the scale to 0.5. We can now repeat everything with the box and the slope. Just choosing different textures. Great. Now our game is a lot easier on the eyes. We've already achieved a lot of things. So it's wise to save our progress. To do this, you can click here on Scene, save Scene, or press Ctrl+S, or Cmd+S. As we did with the textures, we should create a folder for our map right-click and select new folder. Name it maps, again, all small caps. Confirm and save the map. Now we can finally test our game for the first time, but before, go to the project settings and scroll down to display, window and change the width to 1920 and height to 1080. This will set our game screen resolution to full HD. Also, change the test width to 1920 divided by 1.5 and the height to 1080 divided by 1.5. Yes, you can do math operations in every numerical field in Godot. Pretty handy. This last step will affect our resolution only while testing. Close the project settings and click play here. Godot will ask for the main scene. We should select the map scene and done. Obviously, there is something wrong. The problem is that we didn't assign any camera to the game yet. So we staring at this ugly gray screen. We will do this, but let's create our player first. This time we will not choose the basic 3D Scene as the root node for our player. Godot has a specific node that is especially suited for what we need. Click on other node then type body. There are three main types of bodies in Godot. The static body, which you should use to well, static objects. Rigid body, which is a good option when your character would be constantly affected by physics like the birds, in angry birds and a kinematic body, which is what we want. It handles collisions, and can be controlled by code. Okay. Create a kinematic body and rename it to player with capital P. You get this yellow warning here. If you hover the mouse over it, you will see that it is complaining about not having a collision shape, but before creating one let's create a visual representation for the player. Create a new node and this time search for mesh instance. Rename it to body. Yeah. You know it, capital B. Maybe more often seen than Blender's default cube is a capsule shape in game dev tutorials and we'll not break the pattern here. With the body node selected, go to the inspector, click on mesh, new capsule mesh. Cool. You already have a chubby dead player. Let's bring it to life by clicking on the capsule here and changing it's mid height to 2.5. and rotate it in the X axis by 90 degrees. Now our player is alive and well, but it's burrowed in the ground go to transform configure snap, but this time set the translate step to 0.25. And now move the player up until it aligns with the world. To make it easier to us to know which direction the player is facing you add another node for the player's eyes. Create a new mesh instance node, child of the body node, name it, eyes and in mesh, select new cube mesh. That's a hell of an eye. Let's make it smaller. In the cube mesh, in size, set the X value to 1, Y to 0.5, and Z to 0.5 as well move it out of the player body a bit and up. Perfect. Now to make things look prettier, with the body node selected, go to material, new spatial material, albedo and select a nice looking color for our player. I'll be picking green. The eyes are looking good already. I'll let them be. Good. Let's tackle this yellow warning now. With the player node selected, create a new node and this time search for collision. What do we want here is the collision shape node. Cool. The warning didn't disappear but it has moved. So that's still some progress. With the collision shape node selected. Go to the inspector, click on shape and then new capsule shape. You will create this little wire frame mess here. That's okay. Don't panic. Go to the recently created capsule shape and set its height to 2.5. The same as our player mesh. Rotate it 90 degrees in the X-axis and move it up until it aligns with the player's body. Perfect. We still need one more thing though. A camera. And like almost everything in Godot. A camera has its own node. Create one as a child of the player's node, move it up and to the side. In a place you consider good for the player's eyes to be. See this pink wire frame? This is the direction the camera is facing. So fix it, if it needs. Check current. And we are done with the player for now. Let's save it. Ctrl+S or Cmd+S. Go back to the root folder and create a new folder called actors. Inside it, a folder called player. And save it. Good. Now let's add our player to the map scene, click here on the map scene and the viewport will change to our map. To add a scene you've already created inside another scene, you should add an instance of it. To do that, click here in this chain icon, or press Ctrl+Shift+A or Cmd+Shift+A and then select the player scene. Excellent. Position the player somewhere and let's test our game once more. We should now see what's the player is seeing. Let's see, hit play and perfect. This is looking great, but it's really boring. We need to add some functionality to our game. In Godot, you can add functionality to a node by attaching a script to it. With our player scene opened, select the root node player, and either click this, this scroll icon here or right click the note and select attach script. A new window will open up, and for now set the template to empty and change the path by clicking this folder icon. Select the player folder and save it as player.gd. All small caps. You will be taken to the script editor view, where you can write your code inside Godot, without the need of any third-party application. This first line dictates which object this script extents. In this case, the player is a kinematic body. Great, now that we have a script attached to our player you are ready to add some fun to our game. Before we start coding our player, please let me talk about some methods Godot kindly provides to us. These methods will certainly be present in most of your future scripts. They are. Ready. Process. Physics process. Input. And unhandled input. The ready method is called just after the node and all its children have entered the scene tree. You should put the code in this method when you want it to run when the scene is just loaded, and ready to run. The process method is called every frame. If you need a large to run every frame and do not mess with physics, you should put in the process method. The parameter delta is the difference, in time, between the current frame and the last one. The physics process is similar to the process method being called every frame but it has a big difference. It is synced to the physics engine, which means that while the frame rate of the process method may fluctuate while we are playing. The frame rate of the physics engine is much more stable. Use the physics process if you need to change something in physics like moving the player. For input, we have two important methods. The first is the input method that receives all input Godot reads like keyboard, mouse, joysticks, et cetera. And there's also the unhandled input method, which is only called when the input event triggering it has not been captured by any other input method. Usually, I prefer to handle input in the unhandled input method, rather than the regular method With this little break out of the way Let's focus on implementing our first functionality We'll make the player be able to look around using the mouse. As usual, we have a problem that needs solving. Let me show it. To look around, we rotate our player right and left But what about up and down? We can't rotate the player up and down. Otherwise, its body will also rotate. To solve this, we will create another spatial node child of player and we'll call it Head. Move it to where the camera should be place the camera inside the head node and go to the inspector and reset the camera's transform. Now, if you want to rotate the player sideways, we can rotate its body. But if you want to look him up and down, we will rotate the head node instead. In this way, the player's body will rotate to the side as expected but not up and down. Now that we're set to move on, which method do you think is better to read mouse movement? You're right if you answered the unhandled input method, Add it to the first line. To read an input inside this method We should use the event parameter but it's receiving every type of event that Godot can reads. So we need to check if it is of the type you want to handle at the moment. This line will check if the event is of the type input event mouse motion which has the information of the mouse movement and position Write handle camera rotation, passing the event variable and then two lines later create the method. We will put all the logic for rotating the camera inside this method. The underscore is used to signalize that this method is private and it should not be accessible outside this script. Let's first rotate our player to the sides If you look in the viewport The axis we need to rotate to the sides is the Y-axis. Gladly, the kinematic body has an easy method to do this and it's called rotate Y. The rotate Y method asks for an angle And we can get one by using deg2rad which converts an angle from degrees to radians. What does line does is getting the difference between the mouse last position and its current position, and then converts to radians. But if you try the game now, you'll see two problems. First, the rotation speed is too high, making it almost impossible to control it properly. And second When the mouse gets outside the game window, the rotation stops. Let's fix this issue. Little disclaimer If your camera is inverted, just remove the minus sign before event.relative. To slow down the rotation speed we will need to multiply the angle we get by some small number. We see this number being used in many FPS games, and it's usually called camera or mouse sensitivity. So let's create a variable, called camera sensitivity, of the type float with the default value of 0.05. Have you noticed the export word here? You can put it before variables to make it visible in the inspector. Look, if you select the player's root note, there is a camera sensitivity property now you can change it however you want here, but won't change the variable's default value. This is handy when you want to tweak things without going back to the script. Or when you work with a level designer that doesn't want or shouldn't change your code directly. Continuing, let's multiply the angle by our camera's sensitivity variable that we just have declared and it should be okay now. But before we test the game again, create a ready method before the unhandled input event and write this. What this line is doing, is capturing the mouse making the cursor invisible, and limiting its movement. to only inside our game window. Now press F5 to test the game again. See? Now the camera's rotating much smootly. And we can move the mouse sideways forever and the camera will keep rotating normally. Press Alt+F4 to close the game. Nice. Now we need to rotate the head node up and down To do this, we use the method rotate X, but to the head node instead of the player's To change something in the head node, we need first to get some reference to it. And it is dead simple to do. Leaving two blank lines above the ready function Write onready, var, head, of the type of spatial, equals dollar sign head. What this line does, is saving a reference to the head node inside this variable Back to our camera method. We now can write this line of code, which does the same thing as the line above it. But instead, changes the X rotation of the head node taking into consideration the mouse position in the Y axis But why the axes are different you ask? Let me explain. The camera lives in a 3D environment. So their position, rotation, and scale are all vector3 values but the mouse position corresponds to its position on the screen, which is a two-dimensional plane or vector2. So, when we want to get the mouse, moving to the sides, we need to choose the X axis, but up and down movement, we use the Y axis. In contrast, if we look again at our player, the X axis corresponds to the vertical rotation, while the Y axis to the horizontal rotation. There's one last thing we needed to fix before we wrap this up. If we play the game now, everything should look fine. But if we continue to rotate the camera down, down, and down, you eventually become the girl in the Exorcist, and that's not cool. To fix this, we need to assure that the camera rotation is limited to a specific range. First, let's declare two constants. Just below the first line add: Minimum camera angle of type int equals minus 60, then add another line Max camera angle of type int equals 70. We will use these constants to constraint the camera's vertical rotation to a minimum of minus 60 degrees up to a maximum of 70 degrees Inside our camera method, add one last line. What this clamp function does is to make sure that the value of the first parameter, head rotation X will be between the second parameter, and the third parameter. And by using the constants we've just declared We will make sure that the camera rotates in the Y axis only inside the range we want. Let's see. Cool, cool, cool. Now we have a smooth and precise camera movement controlled by the mouse. Also, don't forget to save our progress. We can look around, but we need to move around as well. Don't you think? Me too. But as always, before we start coding, we need to do some things first. First, we need to set up the physics layers. Basically, we can set up objects to "live" (air quotes) inside a specific physics layers. And then later on, when setting up collisions, we can define how these layers interact with each other. To name your physics layers Go to project. Project settings. Scroll down until you find layer names and select 3D physics. Here, you have 20 layers to name, but we need only two for now. Set the first to world, and the second to characters. Back to the map scene and to make our lives easier, select the root node and add a new child node of the type CSG combiner. Then grab all the CSG meshes we've made and dragged them into it. Now that they're all children of the CSG combiner, we can change collisions in it, and it will be passed down automatically with the CSG combiner selected, go to the inspector, check use collision make sure the first square in collision layer is selected and none squares are selected in collision mask. If you stop the mouse over the squares for a bit. A tooltip will show up, telling you which layer a square represents. The layers you check in collision layer makes the object belong to the layer while the layers you check in collision mask, tell you which layers this object will interact with. In this case, the ground, the box, and the slope exist in the world layer. But don't interact with any layers. Roughly, what this means, is that these objects will not check for collisions with any layers, but other objects may check collisions with them. Let's change the player's collision settings as well. Go to the player scene and with the root node selected, which is a KinematicBody, which extends from PhysicsBody, that can handle collisions and then change the collision layer to characters, removing it from the world layer. Since the player exists in the characters layers only. And finally set the mask to both world and characters layers. Since the player can interact both with the world and other players. With the physics layers and collisions already set up, we can now move on to implementing player movement. The first thing we need to do is set up the input map, go to project, project settings, and then select the input map tab. Here lies every action that Godot will listen to With its corresponding key, button, or other input. Let's create a new action by typing in the action field here forward, in small caps press enter, or click add to confirm. Create every other action we'll need. Left, right, backward, and jump. Now, scroll down to the bottom of the list and you shall see the actions we've just created. Here, in front of forward, click the plus icon and then key. Press the W key and click Ok. Do the same thing to the remaining actions I'll use the standard WASD for movement and the space bar for the jump. You can choose whatever key you want. You can set multiple inputs to one event. For example, we can add the arrow keys to the movement actions, with the same procedure. Now that our input map is set, we're finally ready to start coding our player movement. Back to the player Scene, open up the player script by clicking this script icon and then press this icon here to enter the script editor full-screen mode. The player movement involves physics, right? So we'll need to handle input for it inside of the physics process method. But how the hell do we get input events outside an input method? Luckily for us, Godot likes us. Let me show you. Create a physics process method, and then call for a get movement direction method again with an underscore. And you know, why. Below the camera rotation method, which we can fold it by the way by clicking on this little arrow, create the get movement direction method. First of all, we need a variable to store a direction our input is pointing. We set it to vector3 down, which is our gravity direction. Now, we need a bunch of ifs. Each one, checking one direction that a player may want to go. As you can see, using the input method is action pressed We can check if said action is being pressed, even outside an input method. Let's dissect all these transform basis stuff. Go back to the 3D view and select our player root node. Now take a look at the gizmo. There are three colored arrows, right? Each one corresponds to an axis. The green one to the Y axis, the blue to the Z axis, and the red to the X axis. With that in mind, which axis you should change to move the player forward? The Z axis right? Now, take a look at the bottom left of the screen, when I move the player forward. It decreases. So we can safely say that the direction to move the player forward is negative Z. Which leads then, to being positive Z the direction to move the player backwards. Using the same experiment, we can see that the direction to move the player to the left is negative X, while to the right is positive X. Back to the script, just return the direction variable. Since we're returning the duration variable in our get moving direction. We can retrieve it in the physics process method, saving it to another variable called movement. And before we continue, declare a variable called velocity of type vector3 with a default value of vector3 zero, which stands for zero on all three axes. Again, in the physics process method, where we should handle all player movement give it some space and write these two lines. Lastly, use move and slide to apply the movement to the player. This is a KinematicBody built-in method that handles all the physics calculations regarding the movement itself. Pass the velocity. And we're good to go. Play the game again. We're moving, but we are terribly slow. No one would like to play a game like this. Let's fix it. Applying the same principle as we did with the camera's sensitivity, we should multiply the movement by a number. Create an export variable called speed of type float, with a default value of 10 Now multiply the movement by the speed and test the game again. That's much better. Change the speed value as you see fit. The player movement is good, but we can make it even better. If you play it again, you can notice that as soon as I pressed the W key, the player starts moving. And as soon as I let go of the key, the player stops. The movement is happening too suddenly for my taste. In real life, people slowly accelerate to maximum speed, and then, slowly decelerates to zero. To achieve this, we will need to interpolate the velocity between the last value and the new value by a defining weight. Declare a new exported variable called acceleration, of type float, with a default value of six then change the velocity calculation in the physics process to this new lines of code. Here we use Godot's lerp method, which asks for a current value, a target value, and the weight, to slowly blend the old velocity to the new one. To make it more clear, the first parameter is the velocity current value. Then we pass the new velocity we want to reach. Then a value for our acceleration. Multiplying the acceleration by delta We make sure that the weight will compensate fluctuation in FPS. Play the game again, and you should see a much smoother movement. The player slowly accelerates and decelerates. Much better. Now let's implement the player jump. First we'll need two variables, one for the gravity and one for the impulse that we'll apply to our player when the jump key is pressed Declare another constant called gravity, of type int, and a default value of minus 20. If you noticed, constant names are written in all capital letters. This is used across many languages and basically tells you that if you see a variable named like this, you should not change its value in runtime. Next, declare a new exported variable called jump impulse of type float, with a default value of 12. Good. Now we have everything we need to make our player jump. Go to the, get movement direction method, and add the check for the jump action. When the jump key is pressed, we want to set the Y velocity to the jump impulse. Why the Y velocity? If you go back to the 3D view and select the player node, you'll see that the green arrow pointing up is the Y axis. If we would test the game right now, as soon as you hit the jump key, the player would jump in the air and go to the infinite and beyond, never returning. We don't want that. Do we? Nooooooooo. Well, let's save this poor man's soul. Back to the player script, go to the physics process method and add a new line before the move and slide In this line, you add the gravity, eased it by the delta value, each frame. So, in every frame, a force, will be trying to pull the player down, as in real life. Let's try it game again. Way better, but not so funny anymore. We still need to address two problems. Firstly, if press two keys at the same time, for example forward, and right, you see that the play speed is higher than normal. That's because we didn't normalize the direction we want to go. Let's fixed this. Back to the player script, go to the get movement direction method again and add dot normalized to the direction. Easy. What this does, is assures that each of these vector values are contained between negative one and one. Test the game again, and you'll see that everything is working fine now, but if you think the player is too slow just change the speed value. Now, to the second problem. If you jump and try to jump again, when midair, you will jump and you can jump forever, that's pretty fun, but not what we want. In the get movement direction, add a condition if the player is on the floor. But if you would test the game now, you wouldn't even be able to jump that's because we didn't tell Godot, which direction our ground is facing. So it will think that we're never on the floor. Gladly, this has an easy fix. Go to the physics process method and in the move and slide add a second parameter vector up. What this is doing, is telling Godot that the ground's normal is a vector pointing up. Test the game now. We can't jump in the air anymore. Perfect. You can make your player do a double jump if you want creating a variable counting how many times the player has jumped and adding a new condition when checking if the jump has been pressed. By the way, homework for you. Make your player capable of double jumping. And in the next video, I will show you how I would do it. This movement implementation works, but it's rather simple. And while it can be enough for several games, this won't work for us. For example, test the game again, go to the slope and stop on it. You see that the player slowly slides down. This problem would be fixed easily by using the snap within the move and slide method. But since we're using interpolation to accelerate and decelerate, simply setting snap to true won't work in our case. I will not fix this issue by now because in the next video, we will learn how to implement a finite state machine and a better player controller, in line with more complex games. And that will fix everything. If you want to learn how to implement a finite state machine for an FPS controller from the ground up. Stay tuned to my next video. That's it guys we've reached the end of this video. I hope you liked watching it as much as I enjoyed making it. It was a lot of work, but it was totally worth it. Maybe in the next videos, I won't write a 15-page script and take four days to record everything. While I'm taking too long to record it and edit videos I will be posting new videos on the first Friday of every month. Hopefully, as I get better in this YouTube thing, I would release new videos more frequently. Please comment below what's your thoughts about this tutorial. Give me some feedback on where I could improve. I initially thought to make this series extremely beginner-friendly, but if you think the pace is too slow, the video is too long. Anything really. Let me know. If you liked the content, hit the like button subscribe and activate the bell icon. So you get notified when I release new videos, I'm also on Twitter and Instagram and of course, check my games on Itch.io, including capture wars which was made with many of these techniques, I will teach you in this series. If you want to help me with my game dev journey, please consider paying any amount on my games on Itch.io. Consider reviewing them too. It will immensely help my games reach more people. Thank you. Have a nice day. Cheers.
Info
Channel: Busy Weasel
Views: 37,622
Rating: undefined out of 5
Keywords: gamedev, indiegame, game, fps, multiplayer, godot, tutorial
Id: 2TsttIsW7xE
Channel Id: undefined
Length: 40min 23sec (2423 seconds)
Published: Sat Sep 04 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.