本篇承接[Web]小遊戲 – 連鎖反應,說明遊戲實作細節,部分架構沿用之前的設計,像是等比例顯示,沒有看過的同學,可以參考之前的文章,[javascript]自製記憶遊戲實作細節(1/2),本文包含以下部分
素材讀取進度顯示
眼尖的同學會發現開頭所提到的舊文中,也有進度條功能,這次算是加強版,封裝成一個Class,方便重複使用,主要針對圖片和音樂,原始碼請直接參考這裡,使用方式如下:
//首先定義物件,屬性相當於key,後續可以從key取得 const SOUND_BG1 = 'bg1'; const SOUND_BG2 = 'bg2'; const SOUND_BG3 = 'bg3'; const SOUND_BUTTON_CLICK = 'button_click'; const SOUND_GAME_OVER = 'game_over'; const SOUND_BALL_LESS = 'ball_less'; const SOUND_BALL_MANY = 'ball_many'; const SOUND_BALL_MORE = 'ball_more'; /** * 宣告物件並帶入檔案key和url,屬性名稱用中括號是為了讓變數中的值當成key, * 否則會把變數名稱當成key **/ const assetLoader = new AssetLoader({ [SOUND_BG1]: 'audio/bg1.mp3', [SOUND_BG2]: 'audio/bg2.mp3', [SOUND_BG3]: 'audio/bg3.mp3', [SOUND_BUTTON_CLICK]: 'audio/button_click.mp3', [SOUND_GAME_OVER]: 'audio/gameover.mp3', [SOUND_BALL_LESS]: 'audio/less.mp3', [SOUND_BALL_MANY]: 'audio/many.mp3', [SOUND_BALL_MORE]: 'audio/more.mp3' }); /** * Progress事件在讀取時會不斷觸發,並回傳當前檔案和依照數量計算的百分比 * LoadError事件在讀取失敗時觸發,並回傳失敗的key,然後接著讀取,直到結束 **/ assetLoader .Progress((key, percent, successCount, totalCount)=>{ console.log('key=>'+key+', percent=>'+percent+', successCount=>'+successCount+', totalCount=>'+totalCount); //do what you want }).LoadError((key, errorCount, totalCount)=>{ console.log('key=>'+key+', errorCount=>'+errorCount+', totalCount=>'+totalCount); }).StartUntilEnd(); //AssetSrcDictionary存放原始檔url assetLoader.AssetSrcDictionary[SOUND_BALL_LESS]; //AssetDictionary存放物件,若是音訊可直接播放 assetLoader.AssetDictionary[SOUND_BALL_LESS].play();
Matrix結構
下方是隨機產生出來的陣列和渲染出來的結果對照,直橫方向實際看到和陣列方向是不同的,主要是為了後面消球方便運算
Matrix = [ [1, 1, 3, 5, 5, 4, 3, 5, 5, 3, 3, 3] [5, 1, 5, 1, 4, 4, 2, 5, 4, 1, 1, 2] [5, 1, 3, 2, 3, 3, 3, 1, 2, 5, 1, 5] [1, 2, 4, 2, 4, 3, 2, 5, 2, 3, 3, 4] [3, 2, 1, 4, 5, 5, 2, 4, 4, 3, 2, 4] [4, 4, 3, 3, 4, 1, 5, 1, 1, 1, 3, 3] [5, 4, 4, 1, 4, 1, 2, 1, 2, 2, 4, 2] [4, 1, 4, 2, 2, 3, 1, 5, 4, 5, 2, 2] [2, 4, 4, 3, 5, 1, 1, 4, 5, 1, 3, 5] [1, 4, 2, 3, 2, 5, 5, 4, 5, 4, 3, 3] [2, 4, 4, 2, 4, 1, 1, 4, 2, 3, 5, 3] [5, 1, 1, 2, 1, 3, 5, 4, 5, 4, 4, 5] ]; /** * 建立matrix對應的DOM * */ function renderMatrix(){ var ball_html = ''; for (var i=0;i<MATRIX_WIDTH;i++) { for(var j=0;j<MATRIX_HEIGHT;j++){ var debug_text = i+','+j; ball_html += '<figure class="ball" id="p'+i+'_'+j+'" data-x="'+i+'" data-y="'+j+'" data-no="0" >'+debug_text+'</figure>'; } } MatrixArea.innerHTML = ball_html; }
/*數字和顏色的對照*/ .ball[data-no='1']{background: radial-gradient(circle at 100% 100%, #bd0e13, #ff3953);}/*紅*/ .ball[data-no='2']{background: radial-gradient(circle at 100% 100%, #aaaa06, #d0d309);}/*芥末黃*/ .ball[data-no='3']{background: radial-gradient(circle at 100% 100%, #1f4191, #59a9ff);}/*藍*/ .ball[data-no='4']{background: radial-gradient(circle at 100% 100%, #1a8c3a, #3affce);}/*綠*/ .ball[data-no='5']{background: radial-gradient(circle at 100% 100%, #54476f, #dda2ff);}/*紫*/

同色球尋找
這功能是這次開發最花時間的,因為前後共實作了三種方式,前兩種考慮不周,第三種才順利完成,做法不難,但是要考慮好沒有例外狀況發生,步驟如下:
- 由滑鼠所在的球向四方擴散判斷,new_points為同色點四方向的延伸點,checked_points為已確定為同色,不須要再判斷的點
- 依序判斷new_points中的每一點,若顏色相同,加入checked_points,並把四個方向加入new_points中,要加入前需先判斷是否已在checked_points中,避免重複
- 判斷完後,將該點從new_points移除
- 清單一開始會增長,後來會慢慢收斂,直到清單為空,checked_points內就是所有相連的同色球座標
/** * 從1點開始找相鄰的點,同色的點放入清單,檢查後從清單移除該點, * 接著不斷檢查清單中的點,直到清單為空 * @param int x * @param int y * * @return object[] [x,y]陣列 * */ function findTogetherBall(x,y){ //因為陣列中是數字代表,故實際是判斷數字是否相同 var value = getMatrixValue([x, y]); if(value == 0){return [];} var new_points = [[x,y]]; var checked_points = []; while(new_points.length > 0){ var handle_point = new_points.pop(); var near_point = []; //左邊 if(handle_point[0] > 0){ near_point = [handle_point[0]-1, handle_point[1]] if(getMatrixValue(near_point) == value && !inArray(near_point, checked_points)){ new_points.push(near_point); } } //右邊 if(handle_point[0] < MATRIX_WIDTH-1){ near_point = [handle_point[0]+1, handle_point[1]] if(getMatrixValue(near_point) == value && !inArray(near_point, checked_points)){ new_points.push(near_point); } } //上面 if(handle_point[1] > 0){ near_point = [handle_point[0], handle_point[1]-1]; if(getMatrixValue(near_point) == value && !inArray(near_point, checked_points)){ new_points.push(near_point); } } //下面 if(handle_point[1] < MATRIX_HEIGHT-1){ near_point = [handle_point[0], handle_point[1]+1]; if(getMatrixValue(near_point) == value && !inArray(near_point, checked_points)){ new_points.push(near_point); } } if(!inArray(handle_point, checked_points)){ checked_points.push(handle_point); } } if(checked_points.length == 1){ checked_points.pop(); } return checked_points; } /** * item是否在positions陣列內 * @param object item * @param object[] points * * @return bool */ function inArray(item, points){ return points.some(x=>x[0] == item[0] && x[1] == item[1]); }
剩餘組數
這部分計算方法比較土炮一點,步驟如下:
- 將所有未消失的點標記未判斷
- 逐一執行同色球尋找,並把該次同色球的點全部標為已判斷
- 判斷完計數,就可以取得剩餘球數和剩餘組數
/** * 計算剩餘組數 */ function calSetAmount(){ SetAmount = 0; BallAmount = 0; let unchecked_matrix = []; for (var i=0;i<MATRIX_WIDTH;i++) { unchecked_matrix[i] = []; for(var j=0;j<MATRIX_HEIGHT;j++){ //標記未判斷點位 unchecked_matrix[i][j] = Matrix[i][j] != 0; //統計剩餘球數 if(Matrix[i][j] != 0) {BallAmount++;} } } for (var i=0;i<MATRIX_WIDTH;i++) { for(var j=0;j<MATRIX_HEIGHT;j++){ if(unchecked_matrix[i][j]){ //逐一執行同色球尋找 let points = findTogetherBall(i,j); if(points.length > 0){ SetAmount++; } //把該次同色球的點全部標為已判斷 points.forEach(x=>{ unchecked_matrix[x[0]][x[1]] = false; }); } } } }
消球計算
將同色球消除後,需計算球如何落下,有以下規則
- 同一行將空位往上移動
- 若某一行全部消除了,將該行移到最右邊
/** * 消除點選的同色球,並重新排列 * @param object[] points */ function removeFromMatrix(points){ //將點位歸零 for(var i=0;i<points.length;i++){ setMatrixValue(points[i], 0); } let check_width = MATRIX_WIDTH; for (var i=0;i<check_width;i++) { var row = Matrix[i].filter(function(item, index, array){ return item != 0; }); if(row.length == 0){ //整行都是0,要移到最後一行,並少檢查一行 Matrix.push(Matrix.splice(i, 1)[0]); //因為向前移一行,所以同一行再檢查一次 i--; //最後一行全部是0則不檢查 check_width--; }else{ var changed = false; //若0的上面有顏色,讓他往下移動 while(row.length < MATRIX_HEIGHT){ row.unshift(0); changed = true; } if(changed){ Matrix[i] = row; } } } }
回上一步功能
這部分主要是把每一次操作的Matrix都存在一個UndoMatrix陣列裡面,每操作一次就存一次,唯一要注意的就是,複製Matrix時,避免reference,需要複製每個值,而不能直接放入,不然會被一併修改,可參考這裡
let UndoMatrix = []; //每次消球前,放入UndoMatrix //2維陣列需要使用map處理每一層複製,避免reference UndoMatrix.push(Matrix.map((arr)=>{ return arr.slice(); })); function undo(){ Matrix = UndoMatrix.pop(); ScoreCount.pop(); }
其他
- 這個球看起來有點立體感,主要是參考這裡的做法
- 球消除時使用CSS動畫,如何在動畫結束後,重新繪圖一次Matrix? 參考這裡的做法實作sleep方法,等待特定時間,但仍有風險會不同步,完美的作法是監聽CSS動畫結束事件,有興趣的同學可以自行搜尋
- 背景跟按鈕的花色看起來很酷炫,主要是使用這裡提供的CSS背景
- 背景音樂和音效來源來自這裡和這裡
參考資料
發表迴響