有一天,我看到汽车在路上超速驶过,我想制作一个赛车游戏。
我没有使用原生,而是使用threejs。毕竟,对于较大的3D项目,如果我继续使用原生,我会给自己带来麻烦......
本文解释了这个游戏从0到1的开发过程。没有关于webgl和threejs的特别介绍。没有基础的学生可以和threejs文档一起阅读,或者先学习webgl的基本知识~

以下是操作方法:w,前进a,d左转和右转空间,减速,可以漂移
目前,游戏的碰撞检测尚未完成(未来将进行更新和改进),只有汽车左侧和赛道两侧将进行碰撞测试。细节将在下面提及~你也可以通过自己尝试来找出哪两面。
接下来,我们将从0到1实施这个赛车游戏~
1.游戏准备首先,我们必须选择制作什么游戏。如果这是一个公司级的游戏项目,那么在开发方面基本上别无选择。如果你自己练习,你可以根据自己的喜好来练习。我选择赛车的原因就是一个例子:
首先,这是因为赛车游戏相对简单,不需要太多材料。毕竟,这是一个个人发展,没有专门的设计来提供模型。你必须自己找到模型。
其次,赛车游戏的成本是简单而闭环的。有了汽车和赛道,这实际上是最简单的游戏。
所以我们最终决定制作一个简单的赛车游戏。接下来,我们必须寻找材料。
2.材料准备我在网上搜索了很长时间,发现了一个好的汽车obj文件。它有纹理等,但有些颜色还没有添加。我用搅拌机来完成它。
现在我们有了汽车材料,下一步就是赛道。轨道的最早想法是动态生成它,类似于之前的迷宫游戏。
正式的赛车游戏绝对不能动态生成,因为赛道需要定制,并有许多细节,如纹理风景等。
我们的实践项目不可能这么酷,所以我们可以考虑动态生成。
动态生成的优点是,每次刷新时,您都会播放一张新地图,这可能会更新鲜。
还有两种动态生成方法。一种是使用一块板连续地将其平铺,而一块板的顶点信息是[-1,0,1, 1,0,1, 1,0,-1, -1,0,-1]。
从顶部视图来看,它看起来像这样:
但这个有一个非常糟糕的事情,那就是曲线太粗糙,每条曲线都是直角的,这不是很好看。只是改变一个计划Obj构建了两个模型,即直路和转弯,如图所示。
然后,这两个模型不断被平铺。在2D中,它看起来像这样。
看起来这是可能的,但是!
在实际实施后,我发现它仍然不好!
首先,赛道上没有回头路,因为我们的y轴是固定的,没有上坡或下坡的概念。一旦轨道转弯,新道路与现有道路相遇,它就会变得混乱,成为道路的分叉。
其次,必须对随机性进行大量控制,否则可能会有太频繁的角落,如图所示。
在兼容一段时间后,我发现它真的搞砸了,所以我决定自己建立一个赛道模型,自己有足够的食物和衣服,如图所示。
再说一遍,搅拌机非常有用~
在设计这里的赛道时,有一个角落太难设计了。不减速就不可能在拐角处谈判......我相信你肯定可以通过尝试一圈就能知道它是哪个角落~
3.Threejs准备工作已经完成,下一步是编写代码
我不知道你是否还记得之前的原生webgl开发。这很麻烦,对吧?这次我们使用了threejs,这要方便得多。然而,我仍然不得不说,在联系threejs之前,建议你熟悉原生webgl,否则可能会有很多依赖性,并且图形的一些基础将不扎实。
我们的第一步是创造整个场景世界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);
这些是使用threejs所必需的。这比自己创建程序、着色器以及各种编译和绑定要方便得多。
接下来,我们需要导入模型。上次我写了一个简单的objLoader,这次我们使用threejs附带的那个。
var mtlLoader = new THREE.MTLLoader();mtlLoader.setPath('./assets/');mtlLoader.load('car4.mtl', function(materials) {materials.preload(); var objLoader = new THREE.OBJLoader();objLoader.set材料(材料);objLoader.setPath('./assets/');objLoader.load('car4.obj', function(object) {汽车=物体;car.children.forEach(function(item) {item.castShadow = true;});car.position.z = -20;car.position.y = -5;params.scene.add(汽车);self.car = 汽车;params.cb();},函数(){ console.log('progress');},函数(){ console.log('错误');});});
首先加载mtl文件,生成材料,然后加载obj文件,这非常方便。请注意,在将汽车添加到场景中后,我们需要调整位置。我们世界地面的y轴坐标是-5。
从之前的代码中可以看出,相机的起始z坐标为0,我们最初将汽车的z坐标设置为-20。
以同样的方式,再次导入轨道文件。如果我们此时访问它,我们会发现它完全黑暗,如图所示。
为什么会这样?
上帝说,让光明!轨道和汽车本身没有颜色,因此需要材料和灯光来创造颜色。在原生webgl中创建灯光也很麻烦,需要编写着色器。Threejs非常方便。我们只需要以下代码:
var dirLight = new THREE.DirectionalLight(0xccbbaa,0.5,100);dirLight.position.set(-120,500,-0);dirLight.castShadow = true;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;scene.add(dirLight);var light = new THREE.AmbientLight( 0xccbbaa, 0.1 );scene.add(光);
刷新它,我们的整个世界将变得更光明!
(请注意,在这里我们使用环境光+平行光。我们稍后会把它改成其他灯,原因也会给出),但缺少什么吗?对!
仍然缺少阴影。
但让我们在下一节中谈谈阴影,因为这里阴影的处理并不像光那么简单。
抛开阴影,我们可以理解,一个静态的世界已经完成,有汽车和轨道。
document.body.addEventListener('keydown', function(e) { switch(e.keyCode){ 案例87:// wcar.run = true; 休息; 案例65:// acar.rSpeed = 0.02; 休息; 案例68:// dcar.rSpeed = -0.02; 休息; 案例32://空间car.brake(); 休息;}});document.body.addEventListener('keyup', function(e) { switch(e.keyCode){ 案例87:// wcar.run = false; 休息; 案例65:// acar.rSpeed = 0; 休息; 案例68:// dcar.rSpeed = 0; 休息; 案例32://空间car.cancelBrake(); 休息;}});
我们不使用任何与键盘事件相关的库,我们只是自己写几个键。代码应该仍然易于理解。
按w意味着踩油门,汽车的运行属性设置为true,加速将发生在刻度中;同样,按a会修改rSpeed,汽车的旋转将在刻度中发生变化。
如果(this.run){ this.speed += this.acceleration; if(this.speed > this.maxSpeed) { this.speed = this.maxSpeed;}} 否则 { this.speed -= this.deceleration; if(this.speed < 0) { this.speed = 0;}}var speed = -this.speed;如果(速度 === 0){ 返回;}var rotation = this.dirRotation += this.rSpeed;var speedX = Math.sin(旋转)速度;var speedZ = Math.cos(旋转)速度;this.car.rotation.y = 旋转;this.car.position.z += speedZ;this.car.position.x += speedX;
这非常方便。用一些数学计算来修改汽车的旋转和位置是可以的。这比在原生webgl本身中实现各种转换矩阵要方便得多。然而,您必须知道,threejs的底层仍然通过矩阵改变。
为了简要总结本节,我们使用threejs来完成整个世界的布局,然后使用键盘事件来使汽车移动,但我们仍然缺少很多东西。
4.特点和功能本节主要讨论threejs无法实现或不能由threejs轻松实现的函数。让我们先总结一下第三季度后我们仍然缺乏的能力。a.相机关注b.轮胎详情c.影子d.碰撞检测e.漂移我们一个接一个地去吧。
相机关注刚才我们成功地让汽车移动了,但我们的视角没有移动,汽车似乎正在逐渐远离我们。透视由相机控制。之前我们创建了一个相机,现在我们希望它跟随汽车的运动。相机和汽车之间的关系如下图所示。
相机的旋转对应于汽车的旋转,但无论汽车是转动(旋转)还是移动(位置),它也必须改变相机的位置!
这份信件需要澄清。
camera.rotation.y = 旋转;camera.position.x = this.car.position.x + Math.sin(旋转) 20;camera.position.z = this.car.position.z + Math.cos(旋转) 20;
在汽车的刻度法中,相机的位置是根据汽车本身的位置和旋转计算的。20是汽车不旋转时相机与汽车之间的距离(如第3节开头所述)。最好将代码与上图一起理解。这使得相机能够跟随。
轮胎详情轮胎细节需要体验偏航角度的真实性。如果你不知道偏航角度也没关系,只需将其理解为漂移的真实性,如下所示。
事实上,当正常转弯时,轮胎应该先走,车身应该走第二走,但由于透视问题,我们在这里省略了它。
这里的核心是车身方向和轮胎方向之间的不一致。但诀窍来了。threejs的旋转相对僵化。它不能指定任何旋转轴。要么使用 rotation.xyz 旋转坐标轴,要么使用 rotateOnAxis 选择穿过原点旋转的轴。因此,我们只能与车辆一起旋转轮胎,但不能自行旋转。如图所示。
然后,如果我们想旋转,我们首先需要单独提取轮胎模型,它看起来像这样,如图所示。
然后我们发现轮换没问题,但汽车轮换消失了......然后我们必须建立父母关系。汽车的旋转由父母完成,旋转由轮胎自己完成。
mtlLoader.setPath('./assets/');mtlLoader.load(params.mtl, function(materials) {materials.preload(); var objLoader = new THREE.OBJLoader();objLoader.set材料(材料);objLoader.setPath('./assets/');objLoader.load(params.obj, function(object) {object.children.forEach(function(item) {item.castShadow = true;}); var wrapper = new THREE.Object3D();wrapper.position.set(0,-5,-20);wrapper.add(对象);object.position.set(params.offsetX,0,params.offsetZ);scene.add(包装);self.wheel = 对象;self.wrapper = wrapper;},函数(){ console.log('progress');},函数(){ console.log('错误');});});......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;
图片的演示是这样的:
影子
我们之前跳过阴影,说它们不像光那么简单。事实上,shadow在threejs中实现,这比webgl的原生实现更简单几个级别。让我们看看threejs中阴影的实现。这需要三个步骤。1.光源计算阴影2.对象计算阴影3.物体带有阴影这三个步骤可以使阴影出现在你的场景中。
dirLight.castShadow = true;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.children.forEach(function(item) {item.castShadow = true;});......objLoader.load('ground.obj', function(object) {object.children.forEach(function(item) {item.receiveShadow = true;});
但是!
我们这里有的是动态阴影,可以理解为整个场景在不断变化。这样,threejs中的阴影就更麻烦了,需要一些额外的处理。
首先,我们知道我们的光是平行光。平行光可以被视为阳光,覆盖了整个场景。但阴影不起作用。阴影需要通过正交矩阵来计算!
然后问题来了。我们的整个场景都非常大。如果您想用正交矩阵覆盖整个场景,您的帧缓冲图像也会非常大,否则阴影将非常不现实。事实上,没有必要考虑这一步,因为帧缓冲图像根本不能那么大,而且肯定会卡住。该怎么办?我们必须动态地改变正交矩阵!
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();
我们只考虑了汽车在地面上的阴影,所以正交矩阵只能确保它能完全容纳汽车。墙壁没有被考虑。事实上,根据完美,墙壁也应该有阴影。正交矩阵需要放大。
threejs中没有平行光的镜面反射效果,整辆车也不够生动,所以我试着把平行光换成点光源(感觉像路灯?),然后让点光源一直跟着汽车。
var pointLight = new THREE.PointLight(0xccbbaa, 1, 0, 0);pointLight.position.set(-10,20,-20);pointLight.castShadow = true;scene.add(点光);......this.light.position.set(-10+tempX,20,tempZ);this.light.shadow.camera.updateProjectionMatrix();
总的来说,这看起来好多了。这就是改变之前提到的灯光类型的原因~
影响检查我不知道你是否找到了哪些边缘有碰撞检测,但实际上是这些边缘~
红色边缘和汽车右侧之间有碰撞检测,但碰撞检测非常随机。一旦它击中,它就被视为崩溃......速度直接设置为0,然后重新出现。
它确实很
为了检测汽车和轨道之间的碰撞,我们首先必须将3D转换为2D才能看到它,因为我们这里没有任何上坡或下坡的障碍,这很简单。
二维碰撞,我们可以检测汽车的左侧和右侧以及障碍物的侧面。
首先,我们有轨道的二维数据,然后动态获取汽车的左侧和右侧进行检查。
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:外面[i], y:外面[i+1]}, { x:外面[i+2], y:外面[i+3]})) { 返回i;}} 返回-1;};
这有点类似于相机的概念,但数学更麻烦一些。
对于线对线碰撞检测,我们使用三角形面积法,这是最快的线对线碰撞检测。
函数是LineSegmentIntr(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) { 返回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) { 返回false;} 返回真实;}}
他们见面后会发生什么?虽然我们没有完美的反馈,但我们应该有基本的反馈。当我们将速度设置为0并重新出现时,我们必须正确重置汽车的方向,对吗?否则,玩家将不断碰撞......要重置方向,请使用汽车的原始方向矢量并将其投射到碰撞边缘。生成的向量是重置方向。
函数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); 如果(左){projn = getProjectVector(obj,w.rx,w.ry);} 否则 {projn = getProjectVector(obj,w.lx,w.ly);}projn.vx = -0.5;projn.vy = -0.5; 返回{ vx:projw.vx + projn.vx, vy:projw.vy + projn.vy,};}函数getProjectVector(u,dx,dy){ var dp = u.vx dx + u.vy dy; 返回{ vx:(dp dx), vy:(dp dy)};}
漂移
汽车不会漂移,就像打开一个在线游戏,发现网络电缆坏了。
我们不考虑哪一个更快,漂移还是正常的转弯。有兴趣的学生可以去看看。这很有趣。让我先解释三个结论:1.漂移赛车游戏的核心部分之一(英俊),不可能不做。
2.漂移的核心是,它有更好的出口方向,而不会扭曲汽车前部(省略了其他优点和缺点,因为这在视觉上是最直观的)。
3.互联网上没有现成的漂移算法(无论统一性如何),因此我们需要模拟漂移。
对于模拟,我们首先需要了解漂移原理。你还记得我们之前谈论的偏航角度吗?偏航角度是漂移的视觉体验。
更具体地说,偏航角度意味着当汽车的运动方向与汽车前部的方向不一致时,这种差异被称为偏航角度。因此,我们的模拟漂移需要分两步完成:1.生成一个偏航角度,让玩家在视觉上感受到漂移。
2.离开角落的方向是正确的,让玩家感受到真实性。玩家在漂移后不会觉得转弯更不舒服......
下面我们将模拟这两点。事实上,一旦你知道目的,它仍然很容易模拟。
当产生偏航角度时,我们需要保持两个方向,一个是车身的真实旋转方向,真实旋转,另一个是汽车的真实运动方向,旋转(这就是相机遵循的!
)。
这两个值通常是相同的,但一旦用户按下空格键,它们就会开始改变。
var time = Date.now();this.dirRotation += this.rSpeed;this.realRotation += this.rSpeed;var rotation = this.dirRotation;如果(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;
此时,已经产生了偏航角度。
当用户释放空间时,两个方向必须开始统一。此时,请记住dirRotation必须统一到realRotation,否则漂流出角落的意义将消失。
var time = Date.now();如果(this.isBrake){ this.realRotation += this.rSpeed (this.speed / 2);} 否则 { 如果(这个。真正的旋转!
== this.dirRotation) { this.dirRotation += (this.realRotation - this.dirRotation) / 20000 (this.speed) (time - this.cancelBrakeTime);}}