本篇承接[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背景
- 背景音樂和音效來源來自這裡和這裡
參考資料