[Web]連鎖反應實作細節

本篇承接[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);}/*紫*/

同色球尋找

這功能是這次開發最花時間的,因為前後共實作了三種方式,前兩種考慮不周,第三種才順利完成,做法不難,但是要考慮好沒有例外狀況發生,步驟如下:

  1. 由滑鼠所在的球向四方擴散判斷,new_points為同色點四方向的延伸點,checked_points為已確定為同色,不須要再判斷的點
  2. 依序判斷new_points中的每一點,若顏色相同,加入checked_points,並把四個方向加入new_points中,要加入前需先判斷是否已在checked_points中,避免重複
  3. 判斷完後,將該點從new_points移除
  4. 清單一開始會增長,後來會慢慢收斂,直到清單為空,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]);
}

剩餘組數

這部分計算方法比較土炮一點,步驟如下:

  1. 將所有未消失的點標記未判斷
  2. 逐一執行同色球尋找,並把該次同色球的點全部標為已判斷
  3. 判斷完計數,就可以取得剩餘球數和剩餘組數
/**
* 計算剩餘組數
*/
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;
				});
			}
		}
	}
}

消球計算

將同色球消除後,需計算球如何落下,有以下規則

  1. 同一行將空位往上移動
  2. 若某一行全部消除了,將該行移到最右邊
/**
* 消除點選的同色球,並重新排列
* @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();
}

其他

  1. 這個球看起來有點立體感,主要是參考這裡的做法
  2. 球消除時使用CSS動畫,如何在動畫結束後,重新繪圖一次Matrix? 參考這裡的做法實作sleep方法,等待特定時間,但仍有風險會不同步,完美的作法是監聽CSS動畫結束事件,有興趣的同學可以自行搜尋
  3. 背景跟按鈕的花色看起來很酷炫,主要是使用這裡提供的CSS背景
  4. 背景音樂和音效來源來自這裡這裡

參考資料

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

Create a website or blog at WordPress.com

向上 ↑

%d 位部落客按了讚: