1. 程式人生 > >用 CocosCreator 快速開發推箱子游戲

用 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元件佈局如下:

本遊戲免費提供遊戲原始碼,需要原始碼請關注公眾號『一枚小工』獲取