用webgl打造自己的3D迷宮遊戲

發表於2016-09-19

背景:前段時間自己居然迷路了,有感而發就想到寫一個可以讓人迷路的小遊戲,可以消(bao)遣(fu)時(she)間(hui)

沒有使用threejs,就連glMatrix也沒有用,純原生webgl幹,寫起來還是挺累的,不過程式碼結構還是挺清晰的,註釋也挺全的,點開全文開始迷宮之旅~

畢竟要賺一點PV,所以開頭沒有貼地址,現在貼地址:

github:https://github.com/westAnHui/3Dmaze

線上試玩:https://westanhui.github.io/3Dmaze/3Dmaze2.html

遊戲操作:滑鼠控制方向,w前進,s後退,切記方向鍵沒用啊!

迷宮本身的比較簡陋,沒加光和陰影啥的,挺趕的一個demo。不過這篇文章不是介紹webgl技術為主的,主要是講解整個遊戲開發的情況,let’s go~

 

1、生成2D迷宮

迷宮遊戲嘛,肯定迷宮是主體。大家可以從遊戲中看到,我們的迷宮分為2D迷宮和3D迷宮,首先說2D迷宮,它是3D迷宮的前提

生成迷宮有三種方式

a)深度優先

一言不合貼原始碼:https://github.com/westAnHui/3Dmaze/blob/master/maze1.html

先看一下用深度優先法生成迷宮的圖吧

13-300x227

我們看下迷宮的特點,發現有一條很明顯的主路,是不是能理解演算法名中“深度優先”的含義了。簡單介紹一下演算法的原理:

22-300x173

知道了原理,我們著手來製造2D迷宮~

首先得確定牆和路的關係,考慮到迷宮轉化為3D之後牆立體一點,我們就不要用1px的線來模擬牆了,那樣3D之後不夠飽滿~

這裡我們設定牆的厚度為路的寬度,都是10px,然後我們的底圖應該是這樣子的(注:理解這幅圖最為關鍵):

32

白色部分是路,也可以理解為原理中所說的鄰格,這是可以達到的

灰色部分是牆,這個牆可能會打通,也可能沒有打通

黑色部分是牆,這個牆是不可能打通的!!

如果腦子沒轉過來就看下圖,轉化理解

42

紅線就是玩家的路徑啦,其中我們看到穿過了三個黑色的矩形,這就是上面所說的灰色格,可能打通,也可能沒打通,藍色那塊就是沒打通的情況;而黑色部分正對應上面的黑色格,這是不可能打通的,如果把牆看成一個面(灰色藍色部分再壓縮),黑色就變成一個點,是橫牆與豎牆的交點,玩家不會走交點上面走的~

好,下面就是套演算法的過程啦,寫的過程中我把牆的部分給省略了,全部考慮成路

以寬度來解釋,Canvas寬度390px,有18列的路(20列的牆暫時被我無視),不理解的可以對照圖看一下

initNeighbor方法是獲得鄰格用的,注意最後有一個隨機,將它的鄰格打亂,這樣我們在getNeighbor 中獲取鄰格就很方便了

這裡比較核心,註釋給的也比較全,結合前面的原理圖應該很好懂

再看下maze裡面的findPath方法,在這裡面呼叫的getNeighbor方法

可以看到parent和children屬性是不是本能的就反應起樹的概念了,那不就是深度的思想麼~

核心的程式碼講解了,其他的畫圖部分就不介紹了,在drawPath方法裡面,原理就是先畫一個節點(一個格子),然後它的children格和它打通(前面圖中灰色格子轉為白色),再去畫children格……

注:開頭給的試玩demo用的不是深度優先演算法,下面這個是深度優先生成的迷宮遊戲,可以感受一下,這樣與開頭的有一個對比

https://westanhui.github.io/3Dmaze/3Dmaze1.html

b)廣度優先(prim隨機)

一言不合貼原始碼:https://github.com/westAnHui/3Dmaze/blob/master/maze2.html

再看一下廣度優先生成的迷宮圖~可以和上面的對比一下

16-300x225

前面說的深度優先演算法挺好理解的,人類語言表達出來就是“一直走,能走多遠走多遠,發現不通了,死路了,再回去想想辦法”。

但是,用深度優先演算法在迷宮遊戲中有很致命的一個缺點,就是簡單,那條明顯的主路讓玩家不看2D地圖都能輕鬆的繞出來(路痴退散),這明顯不符合開頭所說的消(bao)遣(fu)時(she)間(hui)的主題,那麼正主來啦~

prim(普里姆)演算法是傳統迷宮遊戲的標準演算法,岔路多,複雜。我覺得有廣度優先的思想,所有自己也稱廣度優先演算法,正好和上一個對應上。貼原理圖~

52-280x300

人類語言表達出來就是“隨機的方式將地圖上的牆儘可能打通”,還記得這個底圖麼,照著這個底圖我解釋一下

62

選擇1為起點,並標記。1的鄰牆有2,3,放入陣列中。

此時陣列[2, 3],隨機選擇一個,比如我們選到了2,2的對面格是4,此時4沒有被標記過,打通2(將2由灰變成白色),並將4標記,並把5,6放入陣列

此時陣列[2, 3, 5, 6],繼續隨機……

結合一下原始碼,發現這次寫法和上次的完全不同了,在深度優先中我們直接沒考慮牆的存在,主體是路(白色的格子),將他們變成樹的結構即可,在後面繪製部分再會考慮牆的位置

而在廣度優先中,我認為主體是牆(灰色的格子),所以演算法中一定要把牆的概念帶上,在initNeighbor方法中路(白色格)的鄰格已經是+2而不是之前的+1了,因為+1是牆(灰色格)

再看重要的getNeighbor方法

看起來我們獲得的是鄰格,但實際上我們要的是掛載在鄰格上的wallX和wallY屬性,所以我們可以把neighbor抽象的就看成是牆!!在下面findPath方法中就是這樣用的

如果感覺有點繞的話可以結合原理圖再慢慢的看程式碼,核心理解的一點就是getNeighbor方法返回的x,y對應是路(白色格),而它的wallX,wallY對應的是牆(灰色格)

畫圖部分很簡單

c)遞迴分割法

這個實在是超級簡單,原理簡單,演算法簡單,我就不介紹啦。一來這個生成的迷宮也超級簡單,一般不用於傳統迷宮遊戲;二來後面還有很多要介紹的,不浪費口水在這了

 

2、生成3D迷宮

此時我們已經有一個2D迷宮,我們可以將其看成是俯檢視,下面就是將其轉化為3D頂點資訊

注:這篇文章不負責介紹webgl!!我也儘量避開webgl知識,通俗一點的介紹給大家~

將2D轉3D,首先非常重要的一點就是座標系的轉化

2D的座標系是這樣的

7-300x233

3D的座標系是這樣的

8-300x128

感覺到蛋疼就對了~後面考慮到攝像機近平面的碰撞計算還得蛋碎呢~

其實這個座標轉換並不難,首先我們先通過2D迷宮獲得牆面的資訊(黑色部分)

下面這段程式碼是獲得橫牆資訊的

結果會得到一個陣列,注意一下注釋中很關鍵的一步,為什麼要大於10

下面兩張圖給你答案

9-300x210

10-300x210

總結就是小於等於10px的橫牆,那它的本體一定是豎牆,10px也是那一行正好看到的,我們就將他們過濾掉了

得到豎牆資訊同理,原始碼可見,我就不貼出來了

下面這段程式碼是2D座標轉化為頂點資訊

乘以120是我3D空間中X軸和Z軸各放大了120倍,沒有寫在模型變換矩陣裡面,Y軸的方法在模型變化矩陣中,不過那不重要。

陣列中三個單位為一點,四個點為一個面,五個面為3D迷宮中一堵牆(底面的不管)

後面是webgl裡面常規操作,各種矩陣、繫結buffer、繫結texture等等balabala,原生webgl寫起來是比較累,無視了光和陰影還要寫這麼多T_T

 

3、攝像機碰撞檢測

如果說前面的程式碼寫著很累看著累,那這裡的就更累了……

攝像機是什麼?在3D中攝像機就是玩家的視角,就是通過滑鼠和w,s來移動的webgl可視區,那麼在2D中攝像機對映為什麼呢?

2D中攝像機就是紅色的那個圈圈的右點,如圖!

111-300x197

那麼大的圈圈只是方便看而已……

碰撞檢測的作用是防止出現透視現象,透視現象如下圖所示:

121-300x236

要介紹透視現象出現的原因,就得先了解一下視錐體,如圖:

131-300x175

看到近平面了嗎,當物體穿過近平面,就會出現透視現象了

14-300x157

我們遊戲中近平面距離是0.1,所以可能看成圍繞原點有一個矩形,只要讓矩形碰到不邊,那就不會出現透視現象

矩形的寬度我設定為2,設大了一些,也沒必要讓玩家貼牆貼的那麼近……

我們通過呼叫攝像機的move方法觸發Role.prototype.update方法

而update方法裡面更新x0,x2,y0,y2就是對應那四個點,這四個點在check方法裡面用到,check通過則移動攝像機,否則不移動

攝像機與牆的整體檢測在Role.prototype.isWall中,注意這裡有兩個引數,cx和cy,這個是方向,確切的說是將要移動的方向,然後我們根據方向,只會從這四個點中取三個來判斷會不會有碰撞

15

每個點的檢測通過Role.prototype.pointCheck方法,通過畫素來判斷的,發現是黑色值(rgb中的r為0)那麼就認為撞上了,會在2D中標記黃色。如果你貼著牆走,就會發現黑色的牆都被染成黃色啦~

 

結語:

寫累死,這還是在把webgl裡面知識點大部分丟掉的情況下。迷宮整體比較簡單,就兩張貼圖,地面也很簡陋,最近需求比較多,很忙,沒太多時間去美化。有興趣的同學可以做一款屬於自己棒棒的迷宮遊戲~

感興趣有疑問的可以留言一起交流~

相關文章