用 threejs 製作一款簡單的賽車遊戲

發表於2017-09-19

最近業務比較忙,但是我們追求3D世界的腳步不能停下來~某天在路上看到一輛輛飛馳而過的汽車,想到要不要弄一個賽車類的遊戲
沒有再用原生,而是使用了threejs,畢竟大點的3D專案,再用原生就是自己給自己找麻煩了……
本文從0到1講解了這個遊戲的開發過程,其中沒有專門的介紹webgl和threejs,沒有基礎的同學可以結合threejs文件一起看,或者先學習一下webgl的基礎知識~
遊戲地址如下:

https://vorshen.github.io/simpleCar/
2-300x210
操作如下:
w前進
a、d左右轉
space減速可漂移

目前遊戲的碰撞檢測沒有做完(後續會更新進行完善),只會進行汽車左邊與賽道中兩條邊進行碰撞檢測。具體哪裡下面會說~大家也可以通過親自試玩來找到哪兩條邊

下面我們就來從0到1去實現這個賽車遊戲~
注意:文中出現的程式碼片段是有作用域的!!為了配合講解上下文而刪減了其他的內容!完整程式碼地址如下:
https://github.com/vorshen/simpleCar

1、遊戲準備

首先我們要選擇做一款什麼遊戲,如果是公司級的遊戲專案,那開發基本是沒有選擇權的。自己做著練手那就可以按自己喜好來了。我之所以選擇賽車來舉例子:
首先是因為賽車遊戲比較簡單,沒有過多的素材要求。畢竟是個人開發,沒有專門的設計大大提供模型,模型得自己去找。
其次是賽車遊戲簡單閉環的成本低,有車,有跑道,能跑起來其實就是一款最簡單的遊戲了
所以最終就決定了做一款一切從簡的賽車遊戲,接下來我們要尋找素材

2、素材準備

在網上扒了很久,找到了一款不錯的汽車obj檔案,貼圖啥的都有,不過有的顏色還沒上,用blender進行補齊一下
3-1-300x217
汽車素材有了,接下來就是賽道的。賽道最早的想法是動態生成,類似之前那個迷宮遊戲一樣
正規的賽車遊戲肯定沒法動態生成,因為賽道都需要定製的,有很多細節的東西,比如貼圖風景之類的。
我們這個練手專案追求不了那麼酷炫,所以可以考慮一下動態生成。
動態生成的好處就是每次重新整理後玩都是一個新的地圖,可能新鮮度會高一些。
動態生成的也有兩種玩法,一種是用一塊板不停的去平鋪,板的頂點資訊
[-1,0,1, 1,0,1, 1,0,-1, -1,0,-1]
用俯檢視看起來就是下面這樣
4-1-288x300
但是這個有一個很不好的,就是彎道太粗糙了,每個彎道都是直角,不怎麼好看。就換一個方案
obj建兩個模型,分別是直道、轉彎,如圖
5-1-198x300
然後這兩個個模型不停的去平鋪
用2D看起來就像下面這樣
6-260x300
看起來這個是可行的,但是!真實實現之後發現還是不好!
首先賽道沒法回頭了,因為我們y軸是固定的,沒有上下坡的概念。一旦賽道回頭新的道路碰到已有的道路就會亂,變成岔路的感覺
其次針對隨機要做很多的控制,否則可能出現彎道過於頻繁,如圖

7-300x300

相容了一會,發現很是操蛋,所以決定還是自己建一個賽道模型,自己動手豐衣足食,如圖
8-300x234
再次安利下blender還是很好用的~
在這裡設計賽道的時候有一個彎道設計的太難了,不減速無法無碰撞過彎……相信試玩一圈肯定能找到是哪一個彎~

3、threejs

準備工作都弄完了,接下來就是擼程式碼啦
不知道之前原生webgl開發大家還記得不,很繁瑣對不對,這次我們用了threejs,可就方便很多了。不過還是要說一下,推薦先把原生webgl弄熟一些再去接觸threejs,否則可能會有很大的依賴性,而且對圖形學的一些基礎會不牢固。

我們第一步建立整個場景世界

這些是使用threejs必須要有的,比我們自己原生去建立program,shader,再各種編譯繫結方便了很多
接下來我們要把模型給匯入進去。上次有寫過一個簡單的objLoader,這次我們用threejs自帶的。

首先載入mtl檔案,生成材質之後再載入obj檔案,非常的方便。注意這裡我們把賽車加入到場景之後要調整一下position.zy,地面在我們這個世界中y軸座標為-5
上一段程式碼可以看出攝像機開始的z座標為0,我們將賽車的z座標初始設定為-20
同理再匯入賽道檔案,此時我們訪問的話,會發現一片漆黑,如圖
9-300x209
這是為什麼呢?

神說要有光!

本身賽道和賽車是沒有顏色的,需要用材質+光出現顏色。原生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、漂移

下面一一道來

攝像機跟隨

剛才我們成功讓賽車移動了起來,但是我們的視角沒有動,車彷彿在漸漸遠離我們。視角是由攝像機控制的,之前我們建立了一個攝像機,現在我們要讓它跟隨著賽車運動。攝像機和賽車的關係如下面這兩幅圖
10

11-300x276

也就是說
攝像機的rotation和賽車的rotation是對應的,但是賽車無論轉向(rotation)還是移動(position)也都得去改變攝像機的position!這個對應關係要弄清楚

在car的tick方法中,根據car本身的position和rotation,去算出camera的位置,20就是當賽車沒有旋轉時,攝像機和賽車的距離(第三節開頭有說過)。程式碼結合上面的圖一起理解好點
這樣就實現了攝像機的跟隨

輪胎細節

輪胎細節需要是為了體驗出偏航角時的真實性,不知道偏航角沒有關係,就理解為漂移時的真實性就好了,如下圖
12-300x183
其實普通轉向的時候,也是應該輪胎先行,車身再動,但我們這邊由於視角的問題就省略掉了
這裡核心就是車身方向和輪胎方向的不一致。不過這時候坑爹的就來了,threejs的rotation比較僵硬,它無法指定任意旋轉軸,要麼就是用rotation.xyz的方式旋轉座標軸,要麼就是rotateOnAxis的方式選擇一條通過原點的軸進行旋轉。所以我們只能對輪胎進行隨車旋轉,無法自轉。如圖
13-272x300
那麼我們想自轉,首先需要把輪胎模型給單獨抽出來,變成這樣,如圖
15-253x300
14-300x223
然後我們發現,自轉可以了,隨車旋轉沒了……那麼我們就要建立一個父級關係,隨車的旋轉是父級去做,自轉是輪胎本身去做的
程式碼如下

圖的演示是這樣的
16-256x300

陰影

之前我們把陰影給跳過了,說沒有光那麼簡單。其實陰影在threejs中實現,本身比webgl原生實現簡單了好幾個level。
看下threejs中陰影的實現,需要三步
1、光源計算陰影
2、物體計算陰影
3、物體承載陰影
這三步就可以讓你的場景中出現陰影
如下程式碼:

但是!我們這裡是動態陰影,可以理解為整個場景都要在不斷的變化。這樣threejs中陰影就麻煩一些了,需要我們進行一些額外的處理。
首先我們知道我們的光是平行光,平行光可以看成太陽光,覆蓋整個場景的。但是陰影不行啊,陰影需要通過正射矩陣去算的!那麼問題來了,我們整個場景非常的大,正射矩陣如果想覆蓋整個場景,你的幀緩衝圖也非常的大,否則陰影會很不真實。其實無需考慮到這一步,因為幀緩衝圖壓根就不能那麼大,一定會卡成狗。
那怎麼辦?我們就得動態的去改變正射矩陣!
整個過程可以理解為這樣的
17-300x128

我們只考慮到了賽車在地面的陰影,所以正射矩陣只保證可以完整包含賽車就可以了。牆壁沒有去考慮,其實按完美來說牆壁也應該有陰影的,需要把正射矩陣拉大一點
但是!threejs中平行光就沒有鏡面反射的效果了,整個賽車曉得不夠生動,所以我就嘗試把平行光改成了點光源(路燈的感覺?),然後讓點光源也一直跟隨著賽車

這樣看起來整體就好了很多,之前說的更換光型別原因也就是在這~

碰撞檢測

不知道大家找到了哪幾條邊有碰撞檢測了沒,其實是這幾條邊~
18-300x169
紅色的這幾條邊和賽車的右邊有碰撞檢測,不過碰撞檢測做的很隨意,一旦碰上了就當作撞毀……直接速度置0重新出現了
確實是偷懶,因為碰撞檢測好搞,但這種賽車碰撞反饋在不接入物理引擎的情況下實在不好搞,要考慮很多,如果單純看成一個圓就會方便很多
所以我這次先給大家說碰撞檢測,如果想有很好的反饋……還是接入成熟的物理引擎比較好
賽車和賽道的碰撞檢測,我們先得把3D轉成2D去看,因為我們這邊也沒什麼障礙物上下坡啥的,簡單嘛
19-300x156
2D碰撞,我們可以去檢測賽車的左右邊和障礙物的邊
20-212x300
首先我們有了賽道的2D資料,再去動態獲得賽車的左右邊,拿去檢測
獲取左邊的程式碼

這個和攝像頭的概念有點型別,不過數學計算上麻煩一些
線和線的碰撞檢測我們就用三角形面積法,最快的線與線碰撞檢測

碰上之後呢?雖然我們沒有完美的反饋,但是基本的也應該有啊,我們將速度置0重新出現,總得把賽車方向重置正確對不對?否則玩家就一直在撞了……重置方向的用賽車本來的方向向量,去投影到碰撞邊,得出的向量就是重置的方向

漂移

賽車沒有漂移,就好像就是開啟一款網路遊戲發現網線斷了一樣
我們這邊不去考慮漂移過彎和正常過彎到底哪個快,有興趣的同學可以查一查,還挺有意思的
先說明三點結論
1、漂移賽車遊戲的核心之一(帥),不做不行
2、漂移一大核心是出彎方向更佳,不需要扭動車頭(其他好處和壞處略,因為這個在視覺上看起來是最直觀的)
3、網上沒有現成很好用的漂移演算法(不考慮unity),所以需要我們來模擬漂移

模擬的話,我們就先要知道漂移的原理,還記得之前我們說的偏航角麼?偏航角就是漂移在視覺上的體驗
規範一點說偏航角就是賽車運動方向和車頭朝向方向不一致時,差異的角就叫做偏航角
所以我們的模擬漂移呢,需要做到兩步
1、產生偏航角,在視覺上讓玩家感受到漂移
2、出彎方向正確,在真實性上讓玩家感受到。總不至於玩家一個漂移後發現過彎更難受……

下面就針對這兩點進行模擬,其實知道了目的,還是很好模擬的
偏航角的產生,我們就要去維護兩個方向,一個是車身真正的旋轉方向realRotation,一個是賽車真正的運動方向dirRotation(攝像機跟隨的也是這個!)
在平時這兩個值都是一樣的,但是一旦使用者按下space,就要開始有所變化

此時偏航角已經產生
使用者鬆開space的時候,兩個方向要開始統一,此時切記一定是dirRotation朝著realRotation統一的,否則漂移出彎的意義就沒啦

結尾

時間關係,寫的不是那麼細節,不過核心的地方基本上也都寫了,如果有問題可以留言討論~
這個遊戲還是有非常多的不足與缺陷,後續我還會優化完善,感興趣的同學可以持續關注~
感謝您的閱讀~

相關文章