Recently, our business is busy, but we can’t stop pursuing the 3D world. One day, we saw a car speeding by on the road, and decided to build a racing game

Instead of using native, I used Threejs, after all, for bigger 3D projects, using native is just asking for trouble…

This article explains the development process of this game from 0 to 1, there is no special introduction to WebGL and Threejs, students who do not have the basic knowledge can read together with threejs documentation, or learn the basic knowledge of WebGL ~

The game address is as follows:



vorshen.github.io/simpleCar/



The operation is as follows:

W to go forward

A, D turn left and right

Space deceleration can drift

Currently, collision detection in the game has not been completed (it will be improved in the future update), only collision detection will be carried out on the left side of the car and two sides of the track. You can also try to find out which two sides

Let’s implement this racing game from 0 to 1. Note: the code snippet is scoped!! Other content has been cut to fit the context! The full code address is github.com/vorshen/sim…

1. Game preparation

First of all, we have to choose what game to make. If it is a company-level game project, there is basically no choice in development. Do it yourself so you can do what you like. I chose racing as an example: First of all, racing games are easy and don’t require much material. After all, it is personal development, there is no special design to provide models, models have to be found. Secondly, the cost of the simple closed-loop racing game is low. It is the simplest game with cars and track, so we finally decided to make a simple racing game. Next, we need to find materials

2. Material preparation

I found a nice car OBJ file with all the textures, but I haven’t added any colors yet. Use Blender to complete it



Now that we have the car material, we have the track.The original idea for the track was dynamic generation, similar to the previous maze game

A proper racing game can’t be dynamically generated because tracks need to be customised, with lots of details like textured landscapes.

We can’t be that cool with this hands-on project, so consider dynamic generation.

The advantage of dynamic generation is that every time you refresh the map, you play a new one, which may make it fresher.

Dynamic generation also has two kinds of play, one is to use a board to tile, board vertex information

[1, 1, 1, 1, 1, 0, 1, 1, 0, 1]

From the top, it looks like this



But there’s one bad thing about this, which isThe corners are too rough. Every corner is a right AngleNot so good. Let’s change the plan

Obj builds two models, namely straight path and turn, as shown in the figure



And then the two models kept tiling

In 2D it looks something like this



It seems to work, but! After the real implementation of the discovery is not good!

First of all, there’s no turning back,Because our Y-axis is fixedThere is no concept of going up and down. As soon as the track turns around and the new road hits the existing road, it’s like a fork in the road

Secondly, a lot of control should be done for randomness, otherwise the curves may be too frequent, as shown in the figure

Compatibility for a while, found that it is very fucked, so I decided to build a track model, do their own food and clothing, as shown in the picture



Amway Blender is still pretty good again

When designing the track here, there was a corner that was too difficult to design. It was impossible to turn without slowing down… I’m sure you can find out which turn it is

3, threejs

All the preparation work is done, the next is the polish code. I don’t know before the native WebGL development, you remember it was very tedious, right? This time we use Threejs, can be a lot more convenient. Still, it is recommended that you familiarize yourself with native WebGL before you approach Threejs, otherwise you may have a lot of dependencies and a shaky foundation in graphics.

Our first step is to create the entire scene world

var scene = new THREE.Scene(); Var camera = new THREE. PerspectiveCamera (90, window. InnerWidth/window. InnerHeight, 0.1, 1000); camera.position.z = 0; camera.position.x = 0; var webGLRenderer = new THREE.WebGLRenderer(); webGLRenderer.setPixelRatio(window.devicePixelRatio); webGLRenderer.setSize(window.innerWidth, window.innerHeight); webGLRenderer.setClearColor(0x0077ec, 1);Copy the code

That’s what you have to have with Threejs, and it’s a lot easier than creating programs, shaders, and all kinds of compiled bindings in our own native way and we’re going to import the model into that. Last time we wrote a simple objLoader, this time we use threejs.

var mtlLoader = new THREE.MTLLoader(); mtlLoader.setPath('./assets/'); // mtlLoader.load('car4.mtl', function(materials) {obj materials.preload(); var objLoader = new THREE.OBJLoader(); objLoader.setMaterials(materials); objLoader.setPath('./assets/'); objLoader.load('car4.obj', function(object) { car = object; car.children.forEach(function(item) { item.castShadow = true; }); car.position.z = -20; car.position.y = -5; params.scene.add(car); self.car = car; params.cb(); }, function() { console.log('progress'); }, function() { console.log('error'); }); });Copy the code

First load MTL file, generate material and then load OBj file, very convenient. Notice here that we have to adjust position.zy after adding the car to the scene, the ground in our world has a y-coordinate of -5

As you can see from the previous code, the camera starts at z zero, and we set the car’s z to -20

Similarly, import the track file again. If we visit it now, we will find complete darkness, as shown in the picture



Why is that?

God said let there be light!

The track and car itself have no color, need to use material + light to appear color. Making light in native WebGL is also troublesome, and it is also convenient to write shader and threejs.

Var dirLight = new THREE.DirectionalLight(0xccbbaa, 0.5, 100); dirLight.position.set(-120, 500, -0); dirLight.castShadow = true; dirLight.shadow.mapSize.width = 1000; // default dirLight.shadow.mapSize.height = 1000; // default dirLight.shadow.camera.near = 2; dirLight.shadow.camera.far = 1000; dirLight.shadow.camera.left = -50; dirLight.shadow.camera.right = 50; dirLight.shadow.camera.top = 50; dirLight.shadow.camera.bottom = -50; scene.add(dirLight); Var light = new THREE.AmbientLight(0xccbbaa, 0.1); scene.add( light );Copy the code

Refresh our whole world brightened up! (Note that we used ambient light + parallel light here, we will change to other light later, the reason will be given), but is there something missing? Right! We’re missing shadows but we’ll talk about shadows in the next video, because shadows are not as easy to deal with as light

Away from the shadows, we can understand that a static world has been completed, with cars and tracks to do events below

document.body.addEventListener('keydown', function(e) {
    switch(e.keyCode) {
        case 87: // w
            car.run = true;
            break;
        case 65: // a
            car.rSpeed = 0.02;
            break;
        case 68: // d
            car.rSpeed = -0.02;
            break;
        case 32: // space
            car.brake();
            break;
    }
});
 
document.body.addEventListener('keyup', function(e) {
    switch(e.keyCode) {
        case 87: // w
            car.run = false;
            break;
        case 65: // a
            car.rSpeed = 0;
            break;
        case 68: // d
            car.rSpeed = 0;
            break;
        case 32: // space
            car.cancelBrake();
            break;
    }
});
 Copy the code

Instead of using a library of keyboard events, just a few keys, we’ll write it naked ourselves. It should make sense to press W to press the accelerator, set the run property of the car to true, and accelerate the tick; Similarly, a pressed rSpeed to change the rotation of the car in the tick. The code is as follows:

if(this.run) {
    this.speed += this.acceleration;
    if(this.speed > this.maxSpeed) {
        this.speed = this.maxSpeed;
    }
} else {
    this.speed -= this.deceleration;
    if(this.speed < 0) {
        this.speed = 0;
    }
}
var speed = -this.speed;
if(speed === 0) {
    return ;
}
 
var rotation = this.dirRotation += this.rSpeed;
var speedX = Math.sin(rotation) * speed;
var speedZ = Math.cos(rotation) * speed;
 
this.car.rotation.y = rotation;
 
this.car.position.z += speedZ;
this.car.position.x += speedX;
 Copy the code

It’s easy to change the rotation and position of car with some math calculations. It’s much more convenient than native WebGL to implement various transformation matrices by itself, but remember that threejs is also changed by matrix. To recap, we used Threejs to lay out the entire world, and then keyboard events to get the car moving, but we’re missing a lot of things.

4. Features and functions

This section focuses on features that Threejs can’t implement or that Threejs can’t easily implement. A) camera following B) tire details C) shadows D) Collision detection E) drift

Camera follow

We managed to get the car moving, but our view didn’t move and the car seemed to be moving away from us. The perspective is controlled by the camera, so we created a camera, and now we’re going to follow the car. The relationship between the camera and the car is shown in the following two pictures





That is to say,

The camera’s rotation is the same as the car’s, but the car must change the camera’s position either when it turns or when it moves. This correspondence needs to be clear

camera.rotation.y = rotation;
camera.position.x = this.car.position.x + Math.sin(rotation) * 20;
camera.position.z = this.car.position.z + Math.cos(rotation) * 20;
 Copy the code

In the tick method of the car, calculate the position of the camera according to the car’s position and rotation. 20 is the distance between the camera and the car when the car is not rotating (mentioned at the beginning of section 3). The code is better understood with the diagram above so that the camera follows

Tyre details

Tire details are needed to experience the authenticity of the yaw Angle. It doesn’t matter if you don’t know the yaw Angle, just understand it as the authenticity of drift, as shown in the following figure



In fact, when the ordinary steering, also should be the tire first, the body then move, but we here because of the perspective of the problem omitted

The core here is the inconsistency between the body direction and the tire direction. The rotation of threejs is rigid. It can’t specify any axis. Either rotate the axis in rotate.xyz or select an axis that passes through the origin. So we can only spin the tire along with the car, not the rotation. As shown in figure



So if we want to spin, first we need to pull out the tire model separately, like this





And then we found that the rotation was fine, but the rotation was gone… So we need to establish a parent relationship, with the car rotation is the parent to do, rotation is the tire itself to do

The following code

mtlLoader.setPath('./assets/'); mtlLoader.load(params.mtl, function(materials) { materials.preload(); var objLoader = new THREE.OBJLoader(); objLoader.setMaterials(materials); objLoader.setPath('./assets/'); objLoader.load(params.obj, function(object) { object.children.forEach(function(item) { item.castShadow = true; }); var wrapper = new THREE.Object3D(); / / parent wrapper. Position. Set (0, 5-20); // Set the location of the tire itself in the parent wrapper.add(object); object.position.set(params.offsetX, 0, params.offsetZ); scene.add(wrapper); self.wheel = object; self.wrapper = wrapper; }, function() { console.log('progress'); }, function() { console.log('error'); }); }); ... this.frontLeftWheel.wrapper.rotation.y = this.realRotation; / / as the rotation of the body with this. FrontRightWheel. Wrapper. Rotation. Y = this. RealRotation; this.frontLeftWheel.wheel.rotation.y = (this.dirRotation - this.realRotation) / 2; / / yaw Angle of rotation enclosing frontRightWheel. Wheel. The rotation, y = (enclosing dirRotation - this. RealRotation) / 2;Copy the code

The diagram looks like this

shadow

We skipped shadows earlier, saying it’s not as easy as light. The fact that shadows are implemented in Threejs is itself several levels simpler than webGL’s native implementation. Shadows in threejs, there are three steps: 1, the light source calculates the shadow, 2, the object calculates the shadow, 3, the object carries the shadow.

dirLight.castShadow = true; / / source computing shadows dirLight. Shadow. MapSize. Width = 1000; dirLight.shadow.mapSize.height = 1000; dirLight.shadow.camera.near = 2; dirLight.shadow.camera.far = 1000; dirLight.shadow.camera.left = -50; dirLight.shadow.camera.right = 50; dirLight.shadow.camera.top = 50; dirLight.shadow.camera.bottom = -50; ... objLoader.load('car4.obj', function(object) { car = object; car.children.forEach(function(item) { item.castShadow = true; // Object (car) calculate shadow}); ... objLoader.load('ground.obj', function(object) { object.children.forEach(function(item) { item.receiveShadow = true; // Object (ground) bearing shadow});Copy the code

But!Here we have dynamic shadows, which means that the whole scene is constantly changing. This makes the shadows in Threejs a little trickier and requires some extra processing.

First we know that our light is parallel light,Parallel light can be viewed as sunlight, covering the whole scene. But the shadow is not ah, the shadow needs to be calculated through the orthomorphic matrix! So the problem is, our whole scene is very large, and if you want to cover the whole scene, your frame buffer is also very large, otherwise the shadows will be unreal. You don’t have to worry about this step, because the frame buffer isn’t that big.

So what? We have toDynamically change the orthomorphic matrix!

The whole process can be interpreted like this

var tempX = this.car.position.x + speedX;
var tempZ = this.car.position.z + speedZ;
 
this.light.shadow.camera.left = (tempZ-50+20) >> 0;
this.light.shadow.camera.right = (tempZ+50+20) >> 0;
this.light.shadow.camera.top = (tempX+50) >> 0;
this.light.shadow.camera.bottom = (tempX-50) >> 0;
this.light.position.set(-120+tempX, 500, tempZ);
this.light.shadow.camera.updateProjectionMatrix();
 Copy the code

We only consider the shadow of the car on the ground, so the ortho matrix is only guaranteed to contain the car completely. The wall is not considered, in fact, the wall should be perfect shadow, need to expand the orthotopic matrix a little bit but! The parallel light in Threejs doesn’t have the mirror reflection effect, the whole car is not very vivid, so I tried to change the parallel light into a point light source. And let the point light follow the car all the time

var pointLight = new THREE.PointLight(0xccbbaa, 1, 0, 0); pointLight.position.set(-10, 20, -20); pointLight.castShadow = true; scene.add(pointLight); ... this.light.position.set(-10+tempX, 20, tempZ); this.light.shadow.camera.updateProjectionMatrix();Copy the code

This looks like a whole lot better, said before the reason for changing the light type is also in this ~

Collision detection

I don’t know if you found which edges have collision detection, in fact, these edges ~



The red edges and the right side of the car have collision detection, but collision detection is done haphazard, once you hit it, it’s a crash… Direct velocity zero is back

It’s lazy, because collision detection is easy to do, but this kind of car crash feedback is really hard to do without a physics engine, it’s a lot to think about, it’s a lot easier to think about it as a circle

So I’m going to start with collision detection, if you want to have good feedback… Better to plug in a mature physics engine

Collision tests of the car and the track, we’ll have to start3D to 2DBecause we don’t have any obstacles going up and down, easy



2D collision, we can detect the left and right side of the car and the side of the obstacle



First we have 2D data of the track, then we dynamically obtain the left and right sides of the car for testing

Get the code on the left

Var tempA = -(this.car.rotation. Y + 0.523); this.leftFront.x = Math.sin(tempA) * 8 + tempX; this.leftFront.y = Math.cos(tempA) * 8 + tempZ; TempA = -(this.car.rotation. Y + 2.616); this.leftBack.x = Math.sin(tempA) * 8 + tempX; this.leftBack.y = Math.cos(tempA) * 8 + tempZ; ... Car.prototype.physical = function() { var i = 0; for(; i < outside.length; i += 4) { if(isLineSegmentIntr(this.leftFront, this.leftBack, { x: outside[i], y: outside[i+1] }, { x: outside[i+2], y: outside[i+3] })) { return i; } } return -1; };Copy the code

This is a little bit like the camera concept, but mathematically it’s a little bit more difficult to do line to line collision detection and we’re going to use the triangle area method, the fastest line to line collision detection

function isLineSegmentIntr(a, b, c, d) {
    // console.log(a, b);
    var area_abc = (a.x - c.x) * (b.y - c.y) - (a.y - c.y) * (b.x - c.x); 
 
    var area_abd = (a.x - d.x) * (b.y - d.y) - (a.y - d.y) * (b.x - d.x); 
 
    if(area_abc * area_abd > 0) { 
        return false; 
    }
 
    var area_cda = (c.x - a.x) * (d.y - a.y) - (c.y - a.y) * (d.x - a.x); 
 
    var area_cdb = area_cda + area_abc - area_abd ; 
    if(area_cda * area_cdb > 0) { 
        return false; 
    } 
 
    return true;
}
 Copy the code

And after that? We don’t have perfect feedback, but we should have basic feedback, we reset the speed to zero, we always reset the car’s orientation, right? Otherwise the player will keep bumping into each other… Reorientation takes the car’s original orientation vector, projects it onto the collision side, and the resulting vector is the reorientation

function getBounceVector(obj, w) { var len = Math.sqrt(w.vx * w.vx + w.vy * w.vy); w.dx = w.vx / len; w.dy = w.vy / len; w.rx = -w.dy; w.ry = w.dx; w.lx = w.dy; w.ly = -w.dx; var projw = getProjectVector(obj, w.dx, w.dy); var projn; var left = isLeft(w.p0, w.p1, obj.p0); if(left) { projn = getProjectVector(obj, w.rx, w.ry); } else { projn = getProjectVector(obj, w.lx, w.ly); } projn. Vx * = 0.5; Projn. Vy * = 0.5; return { vx: projw.vx + projn.vx, vy: projw.vy + projn.vy, }; } function getProjectVector(u, dx, dy) { var dp = u.vx * dx + u.vy * dy; return { vx: (dp * dx), vy: (dp * dy) }; }Copy the code

drift

The car didn’t drift, like is to open a network game found the cable is broken Our side not to think about the drift cornering and normal cornering which is fast, interested students can check and also interesting to illustrate three conclusions 1, drifting car racing game (handsome), one of the core of not doing no 2, drift a large core is the bending direction is better, 3. There is no readily available drift algorithm on the web (not considering Unity), so if we need to simulate drift simulation, we should first know the principle of drift, remember the yaw Angle we said before? Yaw Angle is drift in the visual experience Specification point drift Angle is the direction and car racing towards direction is inconsistent, the Angle of the differences is called the yaw Angle So simulation of drift, we need to do two steps 1, produce yaw Angle, let the players feel drift on the vision 2, a bend in the right direction, let the players feel on authenticity. It doesn’t have to be worse for the player after a drift… The realRotation direction of the car body and the real motion direction of the car dirRotation. (the camera follows this too!) Normally these two values are the same, but once the user presses space, they start to change

var time = Date.now();
 
this.dirRotation += this.rSpeed;
this.realRotation += this.rSpeed;
 
var rotation = this.dirRotation;
 
if(this.isBrake) {
    this.realRotation += this.rSpeed * (this.speed / 2);
}
 
this.car.rotation.y = this.realRotation;
this.frontLeftWheel.wrapper.rotation.y = this.realRotation;
this.frontRightWheel.wrapper.rotation.y = this.realRotation;
this.frontLeftWheel.wheel.rotation.y = (this.dirRotation - this.realRotation) / 2;
this.frontRightWheel.wheel.rotation.y = (this.dirRotation - this.realRotation) / 2;
 
camera.rotation.y = this.dirRotation;
 Copy the code

When the user releases the space, the two directions should start to align. Remember that the dirRotation must align with the realRotation, otherwise the meaning of drifting out of the corner will be lost

var time = Date.now(); if(this.isBrake) { this.realRotation += this.rSpeed * (this.speed / 2); } else { if(this.realRotation ! == this.dirRotation) { this.dirRotation += (this.realRotation - this.dirRotation) / 20000 * (this.speed) * (time - this.cancelBrakeTime); // It is related to the speed and time, parameters can be adjusted}}Copy the code

At the end

There are still a lot of deficiencies and defects in this game. I will optimize and improve it later. Interested students can continue to pay attention to ~ thank you for reading ~