我嘗試用Unity克隆了《紀念碑谷》

《紀念碑谷》(Monument Vally)是一款由Ustwo在2014年發行的解謎遊戲。在遊戲中,玩家點擊引導主角「艾達」在一個個由「錯視」效果組成的幾何體迷宮中行走,達到每一關的目的地,遊戲因巧妙的關卡設計和極簡主義的美術風格受到廣泛好評。

幾年前我開始學習Unity引擎的契機,而開始自己的遊戲開發之旅,是因為看到《紀念碑谷》所使用的引擎也是Unity,所以我一直想自己動手把遊戲中的視覺錯位機制克隆出來。

我使用的Unity版本為5.6,如果不想看前面的實現算法,可以直接跳到文章最後看成果視頻。

1.尋路系統:

為了重現遊戲的玩法,我首要解決的問題就是要讓主角艾達能夠正確地移動到滑鼠點擊的位置,實現尋路的方法有很多,Unity自帶了基於模型網格烘焙的尋路系統,經典的基於格子的A星算法,etc 以及我這次選擇使用的寬度優先搜索(BFS)算法,來實現一個基礎的尋路系統:

使用這套插件提供的一些基礎模型來完成一個簡單的地圖創建

廣度優先搜索算法跟A星算法一樣,都是在2D平面內基於離散式格子坐標信息來獲取最短路徑的尋路算法,因此,我需要聲明一個字典(dictionary())類型的變量wayPointDic來儲存地圖中的每一塊「格子(WayPoint)」的信息。

字典是一種類似集合(List)的數據類型,與集合通過訪問整數型的索引(index)來獲取其中的元素不同,字典可以通過任意類型的「鍵值(key)」來儲存任意類型的元素,這里我用格子的二維坐標值(vector2)作為鍵值(key)來儲存格子類(WayPoint),基於字典的特性,便可以直接通過訪問每一塊格子的坐標值來獲取格子(WayPoint)。

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

算法的過程 —— 從起點開始,對搜索的每一個格子四個方向{(0,1),(1、0),(0、-1),(-1,0)} 上的格子進行判斷,如果這四個格子其中之一與終點格子的信息一致,便說明已經找到了終點,反之將進行下一輪搜索,直到找到終點。

聲明一個隊列(queue),並從起點開始,將起點周圍四個方向上的格子存儲(Enqueue)進這個隊列,每一次從隊列中取出(Dequeue)一個格子(WayPoint)判斷此格子是不是終點。如果是,則終止搜索,如果不是則以此格子為中心(searchCenter)將其四個方向上的格子繼續放入隊列(queue),由於隊列數據類型的特性是先進先出(first in,first out)。所以即使在一次循環中往隊列(queue)放入再多新的格子,算法也會依次將searchCenter四個方向上的格子取出判斷後,再進行下一輪的搜索,與此同時也要將這一輪搜索的searchCenter標記為已被搜索過(用一個bool變量isExplored便可以標記),以避免將已經搜索過的格子重復放入隊列。把這一個過程的代碼寫入一個While循環中,一旦不滿足While循環中的條件語句(當前的搜索的格子searchCenter就是終點格子,或者隊列queue中的格子已經被全部取出),換言之就是已經找到了終點,或者沒有找到可以到達終點的路徑,循環中的語句會停止運行,算法結束。

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

雖然已經找到了終點,可我還需要知道從起點到終點經過的是哪些格子,所以在上述算法中還需要給每個格子類(WayPoint)聲明一個名為exploredFrom的變量,把每一輪搜索的searchCenter賦值給四個方向上格子,作為它們的父節點格子。當找到終點,算法結束的時候,從終點格子開始回溯它們的「父節點格子」(exploredFrom),直到回溯到起點格子,並依次將這些「父節點格子」放入(Add)一個泛型集合(List)類型變量path中,再將集合path中的元素轉置(path.Reverse())。得到的這個path集合就是從起點到終點所經過的「路徑」。

在此之前,我還需要加入一個條件判斷,用於判斷是否能夠找到終點,如果找不到,就說明不存在可以到達終點的路徑(「障礙物」阻擋形成死路,道路被「河流」斷開等等狀況),此時將跳出循環,不再返回path集合。

最後在角色移動腳本中,利用協程函數遍歷path集合中的所有格子,便可以實現角色依照算法得出的最短路徑,從起點「走到」終點。(此時還沒有做平滑運動)

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

到此我完成了一個基於二維平面的尋路系統,不過我很快意識到,這並不能應付《紀念碑谷》的關卡設計,因為《紀念碑谷》的地圖是「立體的」,不是所有的格子(WayPoint)在同一個XZ平面內,所以在XZ平面內會存在兩個格子坐標重合的情況。而且玩家可以操作關卡中的機關,對地圖中的一些格子進行旋轉,移動,這會導致地圖中的「格子」坐標信息因玩家的操作而隨時改變,目前為止還無法讓主角通過正確的路逕到達終點。我還需要針對這兩個問題對代碼進行修補。

我嘗試用Unity克隆了《紀念碑谷》

我並不打算去大幅修改之前的尋路算法,通過觀察《紀念碑谷》「水宮」關,地圖正中央的可旋轉機關是連接不同區域的關鍵部分。於是我把關卡中的地圖分為若干個獨立「區域」,處於同一獨立「區域」的格子使用一個整型(int)變量type標記為同一個值,當玩家操縱地圖中央的中的機關旋轉(0°,90°,180°,270°),通過對旋轉角度的條件判斷,來實現每一次旋轉對格子type的改變控制。

我嘗試用Unity克隆了《紀念碑谷》

與此同時,給角色控制類聲明一個inType變量,使用射線(Raycast)檢測每一幀角色所在的格子(WayPoint)信息,使角色的inType值與格子的type一致。然後,我需要對之前的尋路算法做點改動,當玩家控制角色移動,調用尋路算法之前,只有與角色的inType值一致的格子(WayPoint)才會被放入(add)wayPointDic(上述算法中用於存儲格子(WayPoint)的字典)只有這些與角色type一樣的格子才會參與尋路算法,這樣便可以避免角色會「飛」到在XZ平面內坐標重合但處於不同高度(Y)的格子上。當玩家旋轉機關,連通道路的時候,機關上所有格子的才會與角色的inType一致,從而實現道路因機關旋轉而連接或者斷開。

利用射線檢測,角色的inType值與當前腳下格子(wayPoint)的type一致

要了解BFS的算法的原理和實現可以看看這篇。

2.還原「潘洛斯三角」

要從視覺效果上還原「潘洛斯三角」的「錯視」效果並不難,將虛擬攝像機(Camera)的投影方式設定為正交透視(Orthographic)模式。雖然此時虛擬攝像機的旋轉量設置正確,不過處於虛擬攝像機近端的方塊會把遠處的方塊遮住,道路看上去並不是「無縫連接」的。

於是我參考了YouTuber Mixandjam的做法,將處於潘洛斯三角「連接」處遠端的方塊的層級(Layer),設定為topRenderer,然後再創建一個攝像機,將攝像機的ClearFlag屬性設置為「Depth Only」,將Culling Mask(剔除遮擋)僅勾選為topRenderer層級,如此一來,層級layer為topRenderer的方塊便可以渲染在所有方塊之上,從視覺上便還原了「潘洛斯三角」的錯視效果。

我嘗試用Unity克隆了《紀念碑谷》

接下來我還要做的就是讓主角能夠「無縫」走過這個「連接處」,這實際上就是個「傳送門」功能,最初我打算在連接處放置一個碰撞器(Collider),當角色進入碰撞器則調用碰撞檢測將角色傳送到「上面」。不過這樣做會出現一個問題,當三角形連接,玩家自然會點擊「上面」的方塊作為移動的目的地,由於這些方塊的type值與玩家當前的inType值並不一致,換言之這些方塊並不參與尋路算法,所以角色是不會有任何反應的,於是我放棄了這種做法。

我給格子(WayPoint)類聲明了一個bool值realPos,默認情況下這個值為True,返回的格子坐標值將是其處於3D空間中的真實世界坐標,當realPos為False時,返回我手動設置的偽坐標值(fakeX,fakeY),此時將處於潘洛斯三角形「上面」部分的方塊(WayPoint)的realPos屬性勾選為false。換言之,這些方塊返回的坐標信息將是我手動設置的偽坐標值,這些偽坐標值與潘洛斯三角形下面的方塊處於相鄰且連接的狀態,尋路算法將判定這條道路是可走的,從而實現角色「無縫」地走到處於潘洛斯三角形「上面」的方塊位置。

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

我嘗試用Unity克隆了《紀念碑谷》

使用Magica Voxel 製作一些簡單的模型,完成對關卡細節的打磨。

我嘗試用Unity克隆了《紀念碑谷》

成果演示

成果演示

項目包下載地址

最後的最後宣傳一下自己獨立製作並即將上架Steam 的忍者動作遊戲:

最後的最後宣傳一下自己獨立製作並即將上架Steam 的忍者動作遊戲:

來源:機核