在今年八月中旬,《指尖大冒險》SNS 遊戲誕生,其具體的玩法是經過點擊屏幕左右區域來控制機器人的前進方向進行跳躍,而階梯是無窮盡的,若遇到障礙物或者是踩空、或者機器人腳下的階磚隕落,那麼遊戲失敗。算法
筆者對遊戲進行了簡化改造,可經過掃下面二維碼進行體驗。數組
該遊戲能夠被劃分爲三個層次,分別爲景物層、階梯層、背景層,以下圖所示。bash
整個遊戲主要圍繞着這三個層次進行開發:dom
而本文主要來說講如下幾點核心的技術內容:性能
下面,本文逐一進行剖析其開發思路與難點。學習
景物層負責兩側樹葉裝飾的渲染,樹葉分爲左右兩部分,緊貼遊戲容器的兩側。優化
在用戶點擊屏幕操控機器人時,兩側樹葉會隨着機器人前進的動做反向滑動,來營造出遊戲運動的效果。而且,因爲該遊戲是無窮盡的,所以,須要對兩側樹葉實現循環向下滑動的動畫效果。動畫
對於循環滑動的實現,首先要求設計提供可先後無縫銜接的場景圖,而且建議其場景圖高度或寬度大於遊戲容器的高度或寬度,以減小重複繪製的次數。ui
而後按照如下步驟,咱們就能夠實現循環滑動:this
用僞代碼描述以下:
12345678910111213141516複製代碼 |
// 設置循環節點transThreshold = stageHeight;// 獲取滑動後的新位置,transY是滑動偏移量lastPosY1 = leafCon1.y + transY; lastPosY2 = leafCon2.y + transY;// 分別進行滑動if leafCon1.y >= transThreshold // 若遇到其循環節點,leafCon1重置位置 then leafCon1.y = lastPosY2 - leafHeight; else leafCon1.y = lastPosY1;if leafCon2.y >= transThreshold // 若遇到其循環節點,leafCon2重置位置 then leafCon2.y = lastPosY1 - leafHeight; else leafCon2.y = lastPosY2;複製代碼 |
在實際實現的過程當中,再對位置變化過程加入動畫進行潤色,無限循環滑動的動畫效果就出來了。
隨機生成階梯是遊戲的最核心部分。根據遊戲的需求,階梯由「無障礙物的階磚」和「有障礙物的階磚」的組成,而且階梯的生成是隨機性。
其中,無障礙階磚組成一條暢通無阻的路徑,雖然整個路徑的走向是隨機性的,可是每一個階磚之間是相對規律的。
由於,在遊戲設定裏,用戶只能經過點擊屏幕的左側或者右側區域來操控機器人的走向,那麼下一個無障礙階磚必然在當前階磚的左上方或者右上方。
用 0、1 分別表明左上方和右上方,那麼咱們就能夠創建一個無障礙階磚集合對應的數組(下面簡稱無障礙數組),用於記錄無障礙階磚的方向。
而這個數組就是包含 0、1 的隨機數數組。例如,若是生成以下階梯中的無障礙路徑,那麼對應的隨機數數組爲 [0, 0, 1, 1, 0, 0, 0, 1, 1, 1]。
障礙物階磚也是有規律而言的,若是存在障礙物階磚,那麼它只能出如今當前階磚的下一個無障礙階磚的反方向上。
根據遊戲需求,障礙物階磚不必定在鄰近的位置上,其相對當前階磚的距離是一個階磚的隨機倍數,距離範圍爲 1~3。
一樣地,咱們能夠用 0、一、二、3 表明其相對距離倍數,0 表明不存在障礙物階磚,1 表明相對一個階磚的距離,以此類推。
所以,障礙階磚集合對應的數組就是包含 0、一、二、3 的隨機數數組(下面簡稱障礙數組)。例如,若是生成以下圖中的障礙階磚,那麼對應的隨機數數組爲 [0, 1, 1, 2, 0, 1, 3, 1, 0, 1]。
除此以外,根據遊戲需求,障礙物階磚出現的機率是不均等的,不存在的機率爲 50% ,其相對距離越遠機率越小,分別爲 20%、20%、10%。
根據階梯的生成規律,咱們須要創建兩個數組。
對於無障礙數組來講,隨機數 0、1 的出現機率是均等的,那麼咱們只須要利用 Math.random()
來實現映射,用僞代碼表示以下:
1234複製代碼 |
// 生成隨機數i,min <= i < maxfunction getRandomInt(min, max) { return Math.floor(Math.random() * (max - min) + min);}複製代碼 |
12345複製代碼 |
// 生成指定長度的0、1隨機數數組arr = [];for i = 0 to len arr.push(getRandomInt(0,2));return arr;複製代碼 |
而對於障礙數組來講,隨機數 0、一、二、3 的出現機率分別爲:P(0)=50%、P(1)=20%、P(2)=20%、P(3)=10%,是不均等機率的,那麼生成無障礙數組的辦法即是不適用的。
那如何實現生成這種知足指定非均等機率分佈的隨機數數組呢?
咱們能夠利用機率分佈轉化的理念,將非均等機率分佈轉化爲均等機率分佈來進行處理,作法以下:
咱們只要反覆執行步驟 4 ,就可獲得知足上述非均等機率分佈狀況的隨機數數組——障礙數組。
結合障礙數組生成的需求,其實現步驟以下圖所示。
用僞代碼表示以下:
1234567891011121314151617181920複製代碼 |
// 非均等機率分佈PiP = [0.5, 0.2, 0.2, 0.1]; // 獲取最小公倍數L = getLCM(P); // 創建機率轉化數組A = [];l = 0;for i = 0 to P.length k = L * P[i] + l while l < k A[l] = i; l++;// 獲取均等機率分佈的隨機數s = Math.floor(Math.random() * L);// 返回知足非均等機率分佈的隨機數return A[s];複製代碼 |
對這種作法進行性能分析,其生成隨機數的時間複雜度爲 O(1) ,可是在初始化數組 A 時可能會出現極端狀況,由於其最小公倍數有可能爲 100、1000 甚至是達到億數量級,致使不管是時間上仍是空間上佔用都極大。
有沒有辦法能夠進行優化這種極端的狀況呢?
通過研究,筆者瞭解到 Alias Method 算法能夠解決這種狀況。
Alias Method 算法有一種最優的實現方式,稱爲 Vose’s Alias Method ,其作法簡化描述以下:
若是有興趣瞭解具體詳細的算法過程與實現原理,能夠閱讀 Keith Schwarz 的文章《Darts, Dice, and Coins》。
根據 Keith Schwarz 對 Vose’s Alias Method 算法的性能分析,該算法在初始化數組時的時間複雜度始終是 O(n) ,並且隨機生成的時間複雜度在 O(1) ,空間複雜度也始終是 O(n) 。
兩種作法對比,明顯 Vose’s Alias Method 算法性能更加穩定,更適合非均等機率分佈狀況複雜,遊戲性能要求高的場景。
在 Github 上,@jdiscar 已經對 Vose’s Alias Method 算法進行了很好的實現,你能夠到這裏學習。
最後,筆者仍選擇一開始的作法,而不是 Vose’s Alias Method 算法。由於考慮到在生成障礙數組的遊戲需求場景下,其機率是可控的,它並不須要特別考慮機率分佈極端的可能性,而且其代碼實現難度低、代碼量更少。
利用隨機算法生成無障礙數組和障礙數組後,咱們須要在遊戲容器上進行繪製階梯,所以咱們須要肯定每一塊階磚的位置。
咱們知道,每一塊無障礙階磚必然在上一塊階磚的左上方或者右上方,因此,咱們對無障礙階磚的位置計算時能夠依據上一塊階磚的位置進行肯定。
如上圖推算,除去根據設計稿測量肯定第一塊階磚的位置,第n塊的無障礙階磚的位置實際上只須要兩個步驟肯定:
其用僞代碼表示以下:
123456複製代碼 |
// stairSerialNum表明的是在無障礙數組存儲的隨機方向值direction = stairSerialNum ? 1 : -1;// lastPosX、lastPosY表明上一個無障礙階磚的x、y軸位置tmpStair.x = lastPosX + direction * (stair.width / 2);tmpStair.y = lastPosY - (stair.height - 26);複製代碼 |
接着,咱們繼續根據障礙階磚的生成規律,進行以下圖所示推算。
能夠知道,障礙階磚必然在無障礙階磚的反方向上,須要進行反方向偏移。同時,若障礙階磚的位置相距當前階磚爲 n 個階磚位置,那麼 x 軸方向上和 y 軸方向上的偏移量也相應乘以 n 倍。
其用僞代碼表示以下:
12345678910複製代碼 |
// 在無障礙階磚的反方向oppoDirection = stairSerialNum ? -1 : 1;// barrSerialNum表明的是在障礙數組存儲的隨機相對距離n = barrSerialNum;// x軸方向上和y軸方向上的偏移量相應爲n倍if barrSerialNum !== 0 // 0 表明沒有 tmpBarr.x = firstPosX + oppoDirection * (stair.width / 2) * n, tmpBarr.y = firstPosY - (stair.height - 26) * n;複製代碼 |
至此,階梯層完成實現隨機生成階梯。
當遊戲開始時,須要啓動一個自動掉落階磚的定時器,定時執行掉落末端階磚的處理,同時在任務中檢查是否有存在屏幕之外的處理,如有則掉落這些階磚。
因此,除了機器人碰障礙物、走錯方向踩空致使遊戲失敗外,若機器人腳下的階磚隕落也將致使遊戲失敗。
而其處理的難點在於:
對於第一個問題,咱們理所固然地想到從底層邏輯上的無障礙數組和障礙數組入手:判斷障礙階磚是否相鄰,能夠經過同一個下標位置上的障礙數組值是否爲1,若爲1那麼該障礙階磚與當前末端路徑的階磚相鄰。
可是,以此來判斷遠處的障礙階磚是不是在同一 y 軸方向上則變得很麻煩,須要對數組進行屢次遍歷迭代來推算。
而通過對渲染後的階梯層觀察,咱們能夠直接經過 y 軸位置是否相等來解決,以下圖所示。
由於不論是來自相鄰的,仍是同一 y 軸方向上的無障礙階磚,它們的 y 軸位置值與末端的階磚是必然相等的,由於在生成的時候使用的是同一個計算公式。
處理的實現用僞代碼表示以下:
12345678910111213複製代碼 |
// 記錄被掉落階磚的y軸位置值thisStairY = stair.y; // 掉落該無障礙階磚stairCon.removeChild(stair);// 掉落同一個y軸位置的障礙階磚barrArr = barrCon.children;for i in barrArr barr = barrArr[i], thisBarrY = barr.y; if barr.y >= thisStairY // 在同一個y軸位置或者低於 barrCon.removeChild(barr);複製代碼 |
那對於第二個問題——判斷階磚是否在屏幕之外,是否是也能夠經過比較階磚的 y 軸位置值與屏幕底部y軸位置值的大小來解決呢?
不是的,經過 y 軸位置來判斷反而變得更加複雜。
由於在遊戲中,階梯會在機器人前進完成後會有回移的處理,以保證階梯始終在屏幕中心呈現給用戶。這會致使階磚的 y 軸位置會發生動態變化,對判斷形成影響。
可是咱們根據設計稿得出,一屏幕內最多能容納的無障礙階磚是 9 個,那麼只要把第 10 個之外的無障礙階磚及其相鄰的、同一 y 軸方向上的障礙階磚一併移除就能夠了。
因此,咱們把思路從視覺渲染層面再轉回底層邏輯層面,經過檢測無障礙數組的長度是否大於 9 進行處理便可,用僞代碼表示以下:
1234567891011複製代碼 |
// 掉落無障礙階磚stair = stairArr.shift();stair && _dropStair(stair);// 階梯存在數量超過9個以上的部分進行批量掉落if stairArr.length >= 9 num = stairArr.length - 9, arr = stairArr.splice(0, num); for i = 0 to arr.length _dropStair(arr[i]);}複製代碼 |
至此,兩個難點都得以解決。
爲何筆者要選擇這幾點核心內容來剖析呢?
由於這是咱們常常在遊戲開發中常常會遇到的問題:
並且,對於階梯自動掉落的技術點開發解決,也可以讓咱們認識到,遊戲開發問題的解決能夠從視覺層面以及邏輯底層兩方面考慮,學會轉一個角度思考,從而將問題解決簡單化。
這是本文但願可以給你們在遊戲開發方面帶來一些啓發與思考的所在。最後,仍是老話,行文倉促,若錯漏之處還望指正,如有更好的想法,歡迎留言交流討論!
另外,本文同時發佈在「H5遊戲開發」專欄,若是你對該方面的系列文章感興趣,歡迎關注咱們的專欄。