用 CocosCreator 快速開發推箱子游戲
遊戲總共分為4個功能模組:
- 開始遊戲(menuLayer)
- 關卡選擇(levelLayer)
- 遊戲(gameLayer)
- 遊戲結算(gameOverLayer)
Creator內元件效果如下:
遊戲開始預設顯示menuLayer,遊戲中,通過控制各個層級的顯示和隱藏,實現不同模組的切換。例如開始遊戲,點選開始以後,觸發回撥函式,切換到遊戲關卡選擇介面,繫結關係如下圖:
實現程式碼如下:
1 // 開始按鈕回撥 2 startBtnCallBack(event, customEventData){ 3 if(this.curLayer == 1){ 4 return; 5 } 6 this.curLayer = 1; 7 8 this.playSound(sound.BUTTON); 9 10 this.menuLayer.runAction(cc.sequence( 11 cc.fadeOut(0.1), 12 cc.callFunc(() => { 13 this.startBtn.stopAllActions(); 14 this.startBtn.scale = 1.0; 15 this.menuLayer.opacity = 255; 16 this.menuLayer.active = false; 17 } 18 ))); 19 20 this.levelLayer.active = true; 21 this.levelLayer.opacity = 0; 22 this.levelLayer.runAction(cc.sequence( 23 cc.delayTime(0.1), 24 cc.fadeIn(0.1), 25 cc.callFunc(() => { 26 this.updateLevelInfo(); 27 } 28 ))); 29 },
其他功能模組實現類似。以下將分4個模組分別講述各個模組的實現。
1. 開始遊戲 menuLayer
開始遊戲模組,開始遊戲後預設顯示,其他模組隱藏,功能實現相對簡單,介面佈局完成以後,開始遊戲按鈕新增響應事件即可,實現程式碼如上,在此介面添加了一個小動畫,讓開始遊戲按鈕不斷的放大縮小,程式碼如下:
1 // 主介面動畫 2 menuLayerAni(){ 3 this.startBtn.scale = 1.0; 4 this.startBtn.runAction(cc.repeatForever(cc.sequence( 5 cc.scaleTo(0.6, 1.5), 6 cc.scaleTo(0.6, 1.0) 7 ))); 8 },
實現後的效果:
2. 關卡選擇 levelLayer
關卡選擇分兩步:第一步,介面顯示,通過配置檔案,載入預製檔案,顯示所有關卡;第二步,根據遊戲情況,更新每一關卡資訊。
2.1 第一步顯示關卡
遊戲中所有關卡置於ScrollView控制元件上,每一個關卡,使用一個預製檔案(levelItem),通過讀取關卡配置檔案,載入所有關卡,載入完成後重新計算ScrollView內容的高度,載入關卡程式碼如下:
1 // 建立關卡介面子元素 2 createLavelItem (){ 3 // 進入關卡level 4 let callfunc = level => { 5 this.selectLevelCallBack(level); 6 }; 7 8 for(let i = 0; i < this.allLevelCount; i++){ 9 let node = cc.instantiate(this.levelItemPrefab); 10 node.parent = this.levelScroll; 11 let levelItem = node.getComponent("levelItem"); 12 levelItem.levelFunc(callfunc); 13 this.tabLevel.push(levelItem); 14 } 15 // 設定容器高度 16 this.levelContent.height = Math.ceil(this.allLevelCount / 5) * 135 + 20; 17 },
下圖即是所有關卡預製的父節點:
預製指令碼掛在到預製上:
2.2 第二步更新關卡
每一個levelItem預製上掛一個levelItem指令碼元件,levelItem指令碼元件負責更新資訊,主要控制是否可點選、通關星數、關卡等級、點選進入,levelItem指令碼元件更新UI程式碼如下:
1 /** 2 * @description: 顯示星星數量 3 * @param {boolean} isOpen 是否開啟 4 * @param {starCount} 星星數量 5 * @param {cc.SpriteAtlas} levelImgAtlas 紋理圖 6 * @param {number} level 關卡 7 * @return: 8 */ 9 showStar(isOpen, starCount, levelImgAtlas, level){ 10 this.itemBg.attr({"_level_" : level}); 11 if(isOpen){ 12 this.itemBg.getComponent(cc.Sprite).spriteFrame = levelImgAtlas.getSpriteFrame("pass_bg"); 13 this.starImg.active = true; 14 this.starImg.getComponent(cc.Sprite).spriteFrame = levelImgAtlas.getSpriteFrame("point" + starCount); 15 this.levelTxt.opacity = 255; 16 this.itemBg.getComponent(cc.Button).interactable = true; 17 } 18 else{ 19 this.itemBg.getComponent(cc.Sprite).spriteFrame = levelImgAtlas.getSpriteFrame("lock"); 20 this.starImg.active = false; 21 this.levelTxt.opacity = 125; 22 this.itemBg.getComponent(cc.Button).interactable = false; 23 } 24 this.levelTxt.getComponent(cc.Label).string = level; 25 },
玩家的通過的資訊,通過配置儲存檔案,儲存玩家通關資訊,分為已通關、剛開啟和未開啟三種狀態,具體實現如下:
1 // 重新整理關卡上的資訊 2 updateLevelInfo(){ 3 let finishLevel = parseInt(cc.sys.localStorage.getItem("finishLevel") || 0); //已完成關卡 4 for(let i = 1; i <= this.allLevelCount; i++){ 5 // 完成的關卡 6 if(i <= finishLevel){ 7 let data = parseInt(cc.sys.localStorage.getItem("levelStar" + i) || 0); 8 this.tabLevel[i - 1].showStar(true, data, this.levelImgAtlas, i); 9 } 10 // 新開的關卡 11 else if(i == (finishLevel + 1)){ 12 this.tabLevel[i - 1].showStar(true, 0, this.levelImgAtlas, i); 13 } 14 // 未開啟關卡圖 15 else{ 16 this.tabLevel[i - 1].showStar(false, 0, this.levelImgAtlas, i); 17 } 18 } 19 },
最終的顯示效果如下圖:
3. 遊戲 gameLayer
遊戲也分為兩步:第一步,顯示介面;第二步,遊戲操作判斷
3.1 顯示介面
遊戲內使用levelConfig.json配置每一關卡資訊,每個關卡遊戲部分由多行多列的方格組成,每一個關卡資訊包含content、allRow、allCol、heroRow、heroCol、allBox屬性,allRow和allCol記錄總共行數和列數,heroRow、heroCol記錄英雄所在位置,allBox記錄箱子的總數,content是核心,記錄每個方格的屬性,根據不同的屬性顯示不同的物體,如牆面、地面、物體、箱子,可以通過修改配置,增加任意關卡。
讀取關卡所有資料,並根據每一個位置的屬性,顯示不同的實物。
根據配置建立關卡資訊
1 // 建立關卡 2 createLevelLayer(level){ 3 this.gameControlLayer.removeAllChildren(); 4 this.setLevel(); 5 this.setCurNum(); 6 this.setBestNum(); 7 8 let levelContent = this.allLevelConfig[level].content; 9 this.allRow = this.allLevelConfig[level].allRow; 10 this.allCol = this.allLevelConfig[level].allCol; 11 this.heroRow = this.allLevelConfig[level].heroRow; 12 this.heroCol = this.allLevelConfig[level].heroCol; 13 14 // 計算方塊大小 15 this.boxW = this.allWidth / this.allCol; 16 this.boxH = this.boxW; 17 18 // 計算起始座標 19 let sPosX = -(this.allWidth / 2) + (this.boxW / 2); 20 let sPosY = (this.allWidth / 2) - (this.boxW / 2); 21 22 // 計算座標的偏移量,運算規則(寬鋪滿,設定高的座標) 23 let offset = 0; 24 if(this.allRow > this.allCol){ 25 offset = ((this.allRow - this.allCol) * this.boxH) / 2; 26 } 27 else{ 28 offset = ((this.allRow - this.allCol) * this.boxH) / 2; 29 } 30 this.landArrays = []; //地圖容器 31 this.palace = []; //初始化地圖資料 32 for(let i = 0; i < this.allRow; i++){ 33 this.landArrays[i] = []; 34 this.palace[i] = []; 35 } 36 37 for(let i = 0; i < this.allRow; i++){ //每行 38 for(let j = 0; j < this.allCol; j++){ //每列 39 let x = sPosX + (this.boxW * j); 40 let y = sPosY - (this.boxH * i) + offset; 41 let node = this.createBoxItem(i, j, levelContent[i * this.allCol + j], cc.v2(x, y)); 42 this.landArrays[i][j] = node; 43 node.width = this.boxW; 44 node.height = this.boxH; 45 } 46 } 47 48 // 顯示人物 49 this.setLandFrame(this.heroRow, this.heroCol, boxType.HERO); 50 },
根據型別建立元素:
1 // 建立元素 2 createBoxItem(row, col, type, pos){ 3 let node = new cc.Node(); 4 let sprite = node.addComponent(cc.Sprite); 5 let button = node.addComponent(cc.Button); 6 sprite.spriteFrame = this.itemImgAtlas.getSpriteFrame("p" + type); 7 node.parent = this.gameControlLayer; 8 node.position = pos; 9 if(type == boxType.WALL){ //牆面,//牆面,命名為wall_row_col 10 node.name = "wall_" + row + "_" + col; 11 node.attr({"_type_" : type}); 12 } 13 else if(type == boxType.NONE){ //空白區域,//牆面,命名為none_row_col 14 node.name = "none_" + row + "_" + col; 15 node.attr({"_type_" : type}); 16 } 17 else{ //遊戲介面,命名為land_row_col 18 node.name = "land_" + row + "_" + col; 19 node.attr({"_type_" : type}); 20 node.attr({"_row_" : row}); 21 node.attr({"_col_" : col}); 22 button.interactable = true; 23 button.target = node; 24 button.node.on('click', this.clickCallBack, this); 25 if(type == boxType.ENDBOX){ //在目標點上的箱子,直接將完成的箱子數加1 26 this.finishBoxCount += 1; 27 } 28 } 29 this.palace[row][col] = type; 30 31 return node; 32 },
遊戲的所有元素,放置在下圖中gameControlLayer的上:
遊戲開始後,顯示的效果如下(第一關,其他關類似)
3.2 遊戲操作判斷
路線計算好後,玩家移動,若玩家點選的是箱子區域,先檢測箱子前方是否有障礙物,若沒有則推動箱子,通過切換地圖的圖片和修改位置型別達到推動箱子的效果。
點選地圖位置,獲取最優路徑,人物跑到指定點,實現如下:
1 // 點選地圖元素 2 clickCallBack : function(event, customEventData){ 3 let target = event.target; 4 //最小路徑長度 5 this.minPath = this.allCol * this.allRow + 1; 6 //最優路線 7 this.bestMap = []; 8 9 //終點位置 10 this.end = {}; 11 this.end.row = target._row_; 12 this.end.col = target._col_; 13 14 //起點位置 15 this.start = {}; 16 this.start.row = this.heroRow; 17 this.start.col = this.heroCol; 18 19 //判斷終點型別 20 let endType = this.palace[this.end.row][this.end.col]; 21 if((endType == boxType.LAND) || (endType == boxType.BODY)){ //是空地或目標點,直接計算運動軌跡 22 this.getPath(this.start, 0, []); 23 24 if(this.minPath <= this.allCol * this.allRow){ 25 cc.log("從起點[", this.start.row, ",", this.start.col, "]到終點[", 26 this.end.row, ",", this.end.col, "]最短路徑長為:", this.minPath, "最短路徑為:"); 27 28 cc.log("[", this.start.row, ",", this.start.col, "]"); 29 for(let i = 0; i< this.bestMap.length;i++){ 30 cc.log("=>[",this.bestMap[i].row,",",this.bestMap[i].col,"]"); 31 } 32 this.bestMap.unshift(this.start); 33 this.runHero(); 34 }else{ 35 console.log("找不到路徑到達"); 36 } 37 } 38 else if((endType == boxType.BOX) || (endType == boxType.ENDBOX)){ //是箱子,判斷是否可以推動箱子 39 //計算箱子和人物的距離 40 let lr = this.end.row - this.start.row; 41 let lc = this.end.col - this.start.col; 42 if((Math.abs(lr) + Math.abs(lc)) == 1){ //箱子在人物的上下左右方位 43 //計算推動方位是否有障礙物 44 let nextr = this.end.row + lr; 45 let nextc = this.end.col + lc; 46 let t = this.palace[nextr][nextc]; 47 if(t && (t != boxType.WALL) && (t != boxType.BOX) && (t != boxType.ENDBOX)){ //前方不是障礙物,也不是牆壁,推動箱子 48 this.playSound(sound.PUSHBOX); 49 //人物位置還原 50 this.setLandFrame(this.start.row, this.start.col, this.palace[this.start.row][this.start.col]); 51 52 //箱子位置型別 53 let bt = this.palace[this.end.row][this.end.col]; 54 if(bt == boxType.ENDBOX){ //有目標物體的箱子型別,還原成目標點 55 this.palace[this.end.row][this.end.col] = boxType.BODY; 56 this.finishBoxCount -= 1; 57 } 58 else{ 59 this.palace[this.end.row][this.end.col] = boxType.LAND; 60 } 61 //箱子位置變成人物圖,但型別儲存為空地或目標點 62 this.setLandFrame(this.end.row, this.end.col, boxType.HERO); 63 64 //箱子前面位置變成箱子 65 let nt = this.palace[nextr][nextc]; 66 if(nt == boxType.BODY){ //有目標點,將箱子型別設定成有目標箱子 67 this.palace[nextr][nextc] = boxType.ENDBOX; 68 this.finishBoxCount += 1; 69 } 70 else { 71 this.palace[nextr][nextc] = boxType.BOX; 72 } 73 this.setLandFrame(nextr, nextc, this.palace[nextr][nextc]); 74 75 this.curStepNum += 1; 76 //重新整理步數 77 this.setCurNum(); 78 79 //重新整理人物位置 80 this.heroRow = this.end.row; 81 this.heroCol = this.end.col; 82 83 this.checkGameOver(); 84 } 85 else{ 86 this.playSound(sound.WRONG); 87 console.log("前方有障礙物"); 88 } 89 } 90 else{ //目標點錯誤 91 this.playSound(sound.WRONG); 92 console.log("目標點錯誤"); 93 } 94 } 95 },
獲取最優路徑演算法:
1 //curPos記錄當前座標,step記錄步數 2 getPath : function(curPos, step, result){ 3 //判斷是否到達終點 4 if((curPos.row == this.end.row) && (curPos.col == this.end.col)){ 5 if(step < this.minPath){ 6 this.bestMap = []; 7 for(let i = 0; i < result.length; i++){ 8 this.bestMap.push(result[i]); 9 } 10 this.minPath = step; //如果當前抵達步數比最小值小,則修改最小值 11 result = []; 12 } 13 } 14 15 //遞迴 16 for(let i = (curPos.row - 1); i <= (curPos.row + 1); i++){ 17 for(let j = (curPos.col - 1); j <= (curPos.col + 1); j++){ 18 //越界跳過 19 if((i < 0) || (i >= this.allRow) || (j < 0) || (j >= this.allCol)){ 20 continue; 21 } 22 if((i != curPos.row) && (j != curPos.col)){//忽略斜角 23 continue; 24 } 25 else if(this.palace[i][j] && ((this.palace[i][j] == boxType.LAND) || (this.palace[i][j] == boxType.BODY))){ 26 let tmp = this.palace[i][j]; 27 this.palace[i][j] = boxType.WALL; //標記為不可走 28 29 //儲存路線 30 let r = {}; 31 r.row = i; 32 r.col = j; 33 result.push(r); 34 35 this.getPath(r, step + 1, result); 36 this.palace[i][j] = tmp; //嘗試結束,取消標記 37 result.pop(); 38 } 39 } 40 } 41 },
4. 遊戲結算 gameOverLayer
遊戲結束後,根據成功推到箱子數,判斷遊戲是否成功,遊戲成功以後,更新關卡資訊即可。
判斷邏輯如下:
1 // 遊戲結束檢測 2 checkGameOver(){ 3 let count = this.allLevelConfig[this.curLevel].allBox; 4 // 全部推到了指定位置 5 if(this.finishBoxCount == count){ 6 this.gameOverLayer.active = true; 7 this.gameOverLayer.opacity = 1; 8 this.gameOverLayer.runAction(cc.sequence( 9 cc.delayTime(0.5), 10 cc.fadeIn(0.1) 11 )); 12 13 // 重新整理完成的關卡數 14 let finishLevel = parseInt(cc.sys.localStorage.getItem("finishLevel") || 0); 15 if(this.curLevel > finishLevel){ 16 cc.sys.localStorage.setItem("finishLevel", this.curLevel); 17 } 18 19 // 重新整理星星等級 20 cc.sys.localStorage.setItem("levelStar" + this.curLevel, 3); 21 22 // 重新整理最優步數 23 let best = parseInt(cc.sys.localStorage.getItem("levelBest" + this.curLevel) || 0); 24 if((this.curStepNum < best) || (best == 0)){ 25 cc.sys.localStorage.setItem("levelBest" + this.curLevel, this.curStepNum); 26 } 27 this.playSound(sound.GAMEWIN); 28 this.clearGameData(); 29 } 30 },
Creator元件佈局如下:
本遊戲免費提供遊戲原始碼,需要原始碼請關注公眾號『一枚小工』獲取