最近业务比较忙,但是我们追求 3D 世界的脚步不能停下来~某天在路上看到一辆辆飞驰而过的汽车,想到要不要弄一个赛车类的游戏
没有再用原生,而是使用了 threejs,毕竟大点的 3D 项目,再用原生就是自己给自己找麻烦了……
本文从 0 到 1 讲解了这个游戏的开发过程,其中没有专门的介绍 webgl 和 threejs,没有基础的同学可以结合 threejs 文档一起看,或者先学习一下 webgl 的基础知识~
游戏地址如下:

https://vorshen.github.io/simpleCar/

操作如下:
w 前进
a、d 左右转
space 减速可漂移

目前游戏的碰撞检测没有做完 (后续会更新进行完善),只会进行汽车左边与赛道中两条边进行碰撞检测。具体哪里下面会说~大家也可以通过亲自试玩来找到哪两条边

下面我们就来从 0 到 1 去实现这个赛车游戏~
注意:文中出现的代码片段是有作用域的!!为了配合讲解上下文而删减了其他的内容!完整代码地址如下:
https://github.com/vorshen/simpleCar

1、游戏准备

首先我们要选择做一款什么游戏,如果是公司级的游戏项目,那开发基本是没有选择权的。自己做着练手那就可以按自己喜好来了。我之所以选择赛车来举例子:
首先是因为赛车游戏比较简单,没有过多的素材要求。毕竟是个人开发,没有专门的设计大大提供模型,模型得自己去找。
其次是赛车游戏简单闭环的成本低,有车,有跑道,能跑起来其实就是一款最简单的游戏了
所以最终就决定了做一款一切从简的赛车游戏,接下来我们要寻找素材

2、素材准备

在网上扒了很久,找到了一款不错的汽车 obj 文件,贴图啥的都有,不过有的颜色还没上,用 blender 进行补齐一下

汽车素材有了,接下来就是赛道的。赛道最早的想法是动态生成,类似之前那个迷宫游戏一样
正规的赛车游戏肯定没法动态生成,因为赛道都需要定制的,有很多细节的东西,比如贴图风景之类的。
我们这个练手项目追求不了那么酷炫,所以可以考虑一下动态生成。
动态生成的好处就是每次刷新后玩都是一个新的地图,可能新鲜度会高一些。
动态生成的也有两种玩法,一种是用一块板不停的去平铺,板的顶点信息
[-1,0,1, 1,0,1, 1,0,-1, -1,0,-1]
用俯视图看起来就是下面这样

但是这个有一个很不好的,就是弯道太粗糙了,每个弯道都是直角,不怎么好看。就换一个方案
obj 建两个模型,分别是直道、转弯,如图

然后这两个个模型不停的去平铺
用 2D 看起来就像下面这样

看起来这个是可行的,但是!真实实现之后发现还是不好!
首先赛道没法回头了,因为我们 y 轴是固定的,没有上下坡的概念。一旦赛道回头新的道路碰到已有的道路就会乱,变成岔路的感觉
其次针对随机要做很多的控制,否则可能出现弯道过于频繁,如图

兼容了一会,发现很是操蛋,所以决定还是自己建一个赛道模型,自己动手丰衣足食,如图

再次安利下 blender 还是很好用的~
在这里设计赛道的时候有一个弯道设计的太难了,不减速无法无碰撞过弯……相信试玩一圈肯定能找到是哪一个弯~

3、threejs

准备工作都弄完了,接下来就是撸代码啦
不知道之前原生 webgl 开发大家还记得不,很繁琐对不对,这次我们用了 threejs,可就方便很多了。不过还是要说一下,推荐先把原生 webgl 弄熟一些再去接触 threejs,否则可能会有很大的依赖性,而且对图形学的一些基础会不牢固。

我们第一步创建整个场景世界

这些是使用 threejs 必须要有的,比我们自己原生去创建 program,shader,再各种编译绑定方便了很多
接下来我们要把模型给导入进去。上次有写过一个简单的 objLoader,这次我们用 threejs 自带的。

首先加载 mtl 文件,生成材质之后再加载 obj 文件,非常的方便。注意这里我们把赛车加入到场景之后要调整一下 position.zy,地面在我们这个世界中 y 轴坐标为-5
上一段代码可以看出摄像机开始的 z 坐标为 0,我们将赛车的 z 坐标初始设置为-20
同理再导入赛道文件,此时我们访问的话,会发现一片漆黑,如图

这是为什么呢?

神说要有光!

本身赛道和赛车是没有颜色的,需要用材质+光出现颜色。原生 webgl 中制作光也比较麻烦,还需要写 shader,threejs 又是很方便啦
我们只需要如下代码:

刷新一下我们整个是世界亮堂堂起来了!(注意,这里我们用的是环境光+平行光,后续我们会改成其他的光,原因也会给出),可是是不是少了一点什么?对!还少了阴影
但是阴影我们放在下一节再说,因为这里阴影的处理没有光那么简单

抛开阴影,我们可以理解为一个静态的世界已经完成了,有赛车有赛道
下面来做事件处理

我们没有用键盘事件相关的库,就几个键,自己裸写一下。代码应该还是很好懂的
按下 w 就意味着踩油门,car 的 run 属性置为 true,tick 中就要进行加速;同理 a 按下修改了 rSpeed,在 tick 中 car 的 rotation 将会有所变化
代码如下:

很方便,配合一些数学计算去修改 car 的 rotation、position 就 ok 了,比原生 webgl 自己实现各种变换矩阵方便多了,不过要知道 threejs 底层也还是通过 matrix 去变化的。
简单总结一下这一节,我们用 threejs 去完成了整个世界的布局,然后通过键盘事件让汽车也可以动起来了,不过我们还缺少很多东西。

4、特性功能

这节主要说的是 threejs 无法实现或者 threejs 无法简单实现的功能。先总结一下第三节结束之后,我们还欠缺的能力
a、摄像机跟随
b、轮胎细节
c、阴影
d、碰撞检测
e、漂移

下面一一道来

摄像机跟随

刚才我们成功让赛车移动了起来,但是我们的视角没有动,车仿佛在渐渐远离我们。视角是由摄像机控制的,之前我们创建了一个摄像机,现在我们要让它跟随着赛车运动。摄像机和赛车的关系如下面这两幅图


也就是说
摄像机的 rotation 和赛车的 rotation 是对应的,但是赛车无论转向 (rotation) 还是移动 (position) 也都得去改变摄像机的 position!这个对应关系要弄清楚

在 car 的 tick 方法中,根据 car 本身的 position 和 rotation,去算出 camera 的位置,20 就是当赛车没有旋转时,摄像机和赛车的距离 (第三节开头有说过)。代码结合上面的图一起理解好点
这样就实现了摄像机的跟随

轮胎细节

轮胎细节需要是为了体验出偏航角时的真实性,不知道偏航角没有关系,就理解为漂移时的真实性就好了,如下图

其实普通转向的时候,也是应该轮胎先行,车身再动,但我们这边由于视角的问题就省略掉了
这里核心就是车身方向和轮胎方向的不一致。不过这时候坑爹的就来了,threejs 的 rotation 比较僵硬,它无法指定任意旋转轴,要么就是用 rotation.xyz 的方式旋转坐标轴,要么就是 rotateOnAxis 的方式选择一条通过原点的轴进行旋转。所以我们只能对轮胎进行随车旋转,无法自转。如图

那么我们想自转,首先需要把轮胎模型给单独抽出来,变成这样,如图


然后我们发现,自转可以了,随车旋转没了……那么我们就要建立一个父级关系,随车的旋转是父级去做,自转是轮胎本身去做的
代码如下

图的演示是这样的

阴影

之前我们把阴影给跳过了,说没有光那么简单。其实阴影在 threejs 中实现,本身比 webgl 原生实现简单了好几个 level。
看下 threejs 中阴影的实现,需要三步
1、光源计算阴影
2、物体计算阴影
3、物体承载阴影
这三步就可以让你的场景中出现阴影
如下代码:

但是!我们这里是动态阴影,可以理解为整个场景都要在不断的变化。这样 threejs 中阴影就麻烦一些了,需要我们进行一些额外的处理。
首先我们知道我们的光是平行光,平行光可以看成太阳光,覆盖整个场景的。但是阴影不行啊,阴影需要通过正射矩阵去算的!那么问题来了,我们整个场景非常的大,正射矩阵如果想覆盖整个场景,你的帧缓冲图也非常的大,否则阴影会很不真实。其实无需考虑到这一步,因为帧缓冲图压根就不能那么大,一定会卡成狗。
那怎么办?我们就得动态的去改变正射矩阵!
整个过程可以理解为这样的

我们只考虑到了赛车在地面的阴影,所以正射矩阵只保证可以完整包含赛车就可以了。墙壁没有去考虑,其实按完美来说墙壁也应该有阴影的,需要把正射矩阵拉大一点
但是!threejs 中平行光就没有镜面反射的效果了,整个赛车晓得不够生动,所以我就尝试把平行光改成了点光源(路灯的感觉?),然后让点光源也一直跟随着赛车

这样看起来整体就好了很多,之前说的更换光类型原因也就是在这~

碰撞检测

不知道大家找到了哪几条边有碰撞检测了没,其实是这几条边~

红色的这几条边和赛车的右边有碰撞检测,不过碰撞检测做的很随意,一旦碰上了就当作撞毁……直接速度置 0 重新出现了
确实是偷懒,因为碰撞检测好搞,但这种赛车碰撞反馈在不接入物理引擎的情况下实在不好搞,要考虑很多,如果单纯看成一个圆就会方便很多
所以我这次先给大家说碰撞检测,如果想有很好的反馈……还是接入成熟的物理引擎比较好
赛车和赛道的碰撞检测,我们先得把 3D 转成 2D 去看,因为我们这边也没什么障碍物上下坡啥的,简单嘛

2D 碰撞,我们可以去检测赛车的左右边和障碍物的边

首先我们有了赛道的 2D 数据,再去动态获得赛车的左右边,拿去检测
获取左边的代码

这个和摄像头的概念有点类型,不过数学计算上麻烦一些
线和线的碰撞检测我们就用三角形面积法,最快的线与线碰撞检测

碰上之后呢?虽然我们没有完美的反馈,但是基本的也应该有啊,我们将速度置 0 重新出现,总得把赛车方向重置正确对不对?否则玩家就一直在撞了……重置方向的用赛车本来的方向向量,去投影到碰撞边,得出的向量就是重置的方向

漂移

赛车没有漂移,就好像就是打开一款网络游戏发现网线断了一样
我们这边不去考虑漂移过弯和正常过弯到底哪个快,有兴趣的同学可以查一查,还挺有意思的
先说明三点结论
1、漂移赛车游戏的核心之一 (帅),不做不行
2、漂移一大核心是出弯方向更佳,不需要扭动车头 (其他好处和坏处略,因为这个在视觉上看起来是最直观的)
3、网上没有现成很好用的漂移算法 (不考虑 unity),所以需要我们来模拟漂移

模拟的话,我们就先要知道漂移的原理,还记得之前我们说的偏航角么?偏航角就是漂移在视觉上的体验
规范一点说偏航角就是赛车运动方向和车头朝向方向不一致时,差异的角就叫做偏航角
所以我们的模拟漂移呢,需要做到两步
1、产生偏航角,在视觉上让玩家感受到漂移
2、出弯方向正确,在真实性上让玩家感受到。总不至于玩家一个漂移后发现过弯更难受……

下面就针对这两点进行模拟,其实知道了目的,还是很好模拟的
偏航角的产生,我们就要去维护两个方向,一个是车身真正的旋转方向 realRotation,一个是赛车真正的运动方向 dirRotation(摄像机跟随的也是这个!)
在平时这两个值都是一样的,但是一旦用户按下 space,就要开始有所变化

此时偏航角已经产生
用户松开 space 的时候,两个方向要开始统一,此时切记一定是 dirRotation 朝着 realRotation 统一的,否则漂移出弯的意义就没啦

结尾

时间关系,写的不是那么细节,不过核心的地方基本上也都写了,如果有问题可以留言讨论~
这个游戏还是有非常多的不足与缺陷,后续我还会优化完善,感兴趣的同学可以持续关注~
感谢您的阅读~

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2017/09/13139/

  1. 2018 年 4 月 1 日

    您好,请问在用 three.js 开发的那个赛车游戏中,如何获取赛道的 2D 数据,是把 3d 模型转换为 2d 数据么,很想知道是如何实现的,以及如何实现对整个赛道的碰撞检测,非常感谢!

  2. 郭立lee 2018 年 3 月 12 日

    加载有点久

  3. Arey 2017 年 10 月 13 日

    加载太久了,没有加载条。

发表评论