2048是以前火過一段時間的休閒數字消除類遊戲,它的玩法很簡單,上手很容易,但是想到要獲得高分卻很難,看似簡單的遊戲卻有着不少得分的技巧,想當初這個遊戲也曾是陪伴我大學課堂的遊戲之一。雖然在得分上有着不少的技巧,但對於開發來講,這實際上是一件至關容易的事情,仔細分析以後就可能大概理清楚這種消除遊戲的邏輯。javascript
這款遊戲仔細想一想就差很少清楚它的大體的思路,遊戲中只有方塊這一個咱們操做的對象,這個對象包含了所在行,所在列,以及方塊顯示的數字三個屬性,這三個屬性足以表達遊戲中的全部效果。除了方塊,其餘的就是遊戲中必不可少的背景圖層,開始及結束場景等。因爲遊戲中須要將4x4的方塊們整齊排列,所以還須要一個四行四列的表格,來呈現咱們的遊戲效果。java
遊戲中最主要的操做就是經過手指觸摸屏幕進行滑屏操做,帶動場景中的方塊總體移動,而且遇到相同數字的方塊進行合併。滑動的邏輯就是遍歷場景中全部的方塊,每個方塊在滑動方向進行移動,若是前方沒有方塊,方塊就一直滑動,若是前方有方塊,判斷本身的數字與這個方塊的數字是否相同,相同進行合併操做,不相同則停在當前位置。git
在對遊戲邏輯進行了分析以後,就能夠用代碼進行實現了,編碼,其實就是一個將遊戲邏輯轉換爲機器語言的過程而已。github
首先,咱們須要一個類來存儲方塊的長度和寬度,代碼以下:數組
// 保存方塊長度方塊 var tile = { width: 0, height: 0 };
方塊類是對場景中可操的方塊的封裝,包括所在行,所在列,顯示數字三個屬性,代碼以下:框架
var Tiled = cc.Node.extend({ num: 0, col: 0, row: 0, ctor: function (num) { this._super(); return true; } }
固然僅僅有以上三個屬性是不夠的,咱們還須要在方塊的構造函數中繪製方塊的背景、繪製方塊顯示的數字、以及隨機設定方塊的行列座標。dom
var Tiled = cc.Node.extend({ num: 0, col: 0, row: 0, ctor: function (num) { this._super(); this.num = num; var count = 0; while (true) { count++; this.row = Math.floor(Math.random() * 4); this.col = Math.floor(Math.random() * 4); if (tiles[this.row][this.col] == null) { tiles[this.row][this.col] = this; break; } if (count >= 16) {// 格子滿了 return true; } } // 繪製背景 var bg = new cc.DrawNode(); bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(255, 209, 145, 255), 1, cc.color(255, 209, 145, 255)); this.addChild(bg); bg.setTag(2); // 繪製數字 var labelNum = new cc.LabelTTF(); labelNum.setString("" + this.num); labelNum.setFontSize(60); // 字體描邊效果 // labelNum.enableStroke(cc.color.BLACK, 0); this.addChild(labelNum); labelNum.setTag(1); // 設定字體和座標 labelNum.setPosition(tile.width / 2, tile.height / 2); // 移動塊 this.newTile(this.row, this.col); return true; } }
除此以外,每一個方塊應該還要包含一個newTile方法、moveTo方法和updateNum方法,分別封裝隨機建立方塊、移動方塊和更新方塊數字三個功能。
在updateNum方法中,咱們主要作兩件事,更新方塊顯示數字和更新方塊背景顏色(在方塊的背景色上,我還專門上網搜了幾種顏色搭配,恩,感受頗有藝術美,哈哈),代碼以下:函數
updateNum: function () { this.getChildByTag(1).setString("" + this.num); var bg = this.getChildByTag(2); switch (this.num) { case 2: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(235, 245, 223, 255), 1, cc.color(235, 245, 223, 255)); break; case 4: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(186, 212, 170, 255), 1, cc.color(186, 212, 170, 255)); break; case 8: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(212, 212, 170, 255), 1, cc.color(212, 212, 170, 255)); break; case 16: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(193, 160, 117, 255), 1, cc.color(193, 160, 117, 255)); break; case 32: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(124, 99, 84, 255), 1, cc.color(124, 99, 84, 255)); break; case 64: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(218, 227, 224, 255), 1, cc.color(218, 227, 224, 255)); break; case 128: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(64, 125, 148, 255), 1, cc.color(64, 125, 148, 255)); break; case 256: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(123, 118, 135, 255), 1, cc.color(123, 118, 135, 255)); break; case 512: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(172, 173, 172, 255), 1, cc.color(172, 173, 172, 255)); break; case 1024: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(204, 196, 194, 255), 1, cc.color(204, 196, 194, 255)); break; case 2048: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(199, 225, 240, 255), 1, cc.color(199, 225, 240, 255)); break; case 4096: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(150, 196, 230, 255), 1, cc.color(150, 196, 230, 255)); break; case 8192: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(25, 77, 91, 255), 1, cc.color(25, 77, 91, 255)); break; case 16384: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(229, 96, 205, 255), 1, cc.color(229, 96, 205, 255)); break; case 32768: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(250, 174, 78, 255), 1, cc.color(250, 174, 78, 255)); break; case 65536: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(255, 241, 222, 255), 1, cc.color(255, 241, 222, 255)); break; default: bg.drawRect(cc.p(5, 5), cc.p(tile.width - 5, tile.height - 5), cc.color(255, 209, 145, 255), 1, cc.color(255, 209, 145, 255)); break; } }
方塊的移動方法和建立方法,我在這裏先寫成同樣,由於尚未對方塊的建立和移動作區別,若是要作優化,能夠在移動時加入移動的動畫,在建立加入建立的動畫,代碼以下:學習
moveTo: function (row, col) { this.row = row; this.col = col; this.setPositionX((cc.winSize.width - tile.width * 4) / 2 + tile.width * this.col); this.setPositionY((cc.winSize.height - tile.height * 4) / 2 + tile.height * this.row); }, newTile: function (row, col) { this.row = row; this.col = col; this.setPositionX((cc.winSize.width - tile.width * 4) / 2 + tile.width * this.col); this.setPositionY((cc.winSize.height - tile.height * 4) / 2 + tile.height * this.row); }
全部的遊戲操做及操做反饋都在遊戲場景類中進行,遊戲場景類將封裝好的方塊類放到遊戲邏輯中,經過玩家操做給予必定的操做反饋。遊戲場景類中主要接受玩家的滑動操做,並在接收到滑動操做後將全部的方塊類進行移動或合併。
遊戲場景類中須要isMove,startX,startY,以及tiles四個屬性:第一個是控制玩家觸摸操做的標識變量,避免重複調用移動方法;中間兩個爲記錄玩家手指滑動的距離,當距離查過必定的長度以後,才判斷玩家進行了滑動操做;最後一個變量是一個數組,用於存儲在4x4的表格中的方塊的信息。
有了以上四個屬性以後,就能夠在構造函數中進行初始化了,代碼以下:字體
var tiles = null;// 存儲方塊信息 var GameLayer = cc.Layer.extend({ isMove: false, startX: 0, startY: 0, ctor: function () { this._super(); this.isMove = false; this.startX = 0; this.startY = 0; //設置塊的寬高 if (cc.winSize.width < cc.winSize.height) { // 豎屏 tile.width = cc.winSize.width / 5; tile.height = cc.winSize.width / 5; } else { // 橫屏 tile.width = cc.winSize.height / 5; tile.height = cc.winSize.height / 5; } // 初始化數組 tiles = [ [null, null, null, null], [null, null, null, null], [null, null, null, null], [null, null, null, null] ]; return true; } }
在初始化完以後,咱們就能夠在場景的onEnter方法中繪製場景了,繪製的內容包括遊戲背景以及隨機初始化兩個方塊,代碼以下:
onEnter: function () { this._super(); // 繪製背景 this.drawBg(); // 繪製塊 var tile1 = new Tiled(2); var tile2 = new Tiled(2); this.addChild(tile1); this.addChild(tile2); //處理事件 cc.eventManager.addListener({ event: cc.EventListener.TOUCH_ONE_BY_ONE, swallowTouches: true, onTouchBegan: this.touchbegan, onTouchMoved: this.touchmoved }, this); return true; }
其中drawBg方法中,繪製了五條垂直直線以及五條水平直線,來做爲方塊所在的4x4的表格,代碼以下:
drawBg: function () { //繪製背景 var bgRect = new cc.DrawNode(); bgRect.drawRect(cc.p(0, 0), cc.p(cc.winSize.width, cc.winSize.height), cc.color(173, 140, 61, 255), 1, cc.color(173, 140, 61, 255)); this.addChild(bgRect); var bg = new cc.DrawNode(); for (var n = 0; n < 5; n++) { bg.drawSegment(cc.p((cc.winSize.width - tile.width * 4) / 2, (cc.winSize.height - tile.height * 4) / 2 + n * tile.width), cc.p(cc.winSize.width / 2 + tile.width * 2, (cc.winSize.height - tile.height * 4) / 2 + n * tile.width), 5, cc.color(55, 62, 64, 255)); bg.drawSegment(cc.p((cc.winSize.width - tile.width * 4) / 2 + n * tile.width, (cc.winSize.height - tile.height * 4) / 2), cc.p((cc.winSize.width - tile.width * 4) / 2 + n * tile.width, (cc.winSize.height - tile.height * 4) / 2 + tile.width * 4), 5, cc.color(55, 62, 64, 255)); } this.addChild(bg); }
能夠看到,在onEnter中,註冊了觸摸事件,這個觸摸事件就用於接受玩家的操做,在TouchBegan事件中記錄觸摸開始時的點,在TouchMoved中記錄當前移動到的點,當觸摸距離超過必定長度時,斷定玩家進行了滑動操做,並經過觸摸點來判斷玩家滑動的舉例,代碼以下:
touchbegan: function (touch, event) { this.isMove = true; this.startX = touch.getLocationX(); this.startY = touch.getLocationY(); return true; }, touchmoved: function (touch, event) { if (!this.isMove) { return; } var endX = touch.getLocation().x; var endY = touch.getLocation().y; if (Math.abs(endX - this.startX) > 20 || Math.abs(endY - this.startY) > 20) { var dir = ""; if (Math.abs(endX - this.startX) > Math.abs(endY - this.startY)) {//左右 if (endX > this.startX) { dir = "right"; } else { dir = "left"; } } else { //上下 if (endY > this.startY) { dir = "up"; } else { dir = "down"; } } this.isMove = false; event.getCurrentTarget().moveAllTiled(dir); } return true; }, moveAllTiled: function (dir) { var isMoved = false; switch (dir) { case "up": isMoved = this.moveUp(); break; case "down": isMoved = this.moveDown(); break; case "left": isMoved = this.moveLeft(); break; case "right": isMoved = this.moveRight(); break; } if (isMoved) { //每次移動產生一個新塊 this.newTiled(); } }
觸動滑動操做後,開始執行滑動邏輯,每次滑動以後,會隨機建立一個新的方塊,在newTiled方法中,除了建立新方塊以外,還須要判斷遊戲是否結束,代碼以下:
newTiled: function () { var tile = new Tiled(2); this.addChild(tile); // 判斷遊戲是否結束 var isOver = true; // 判斷是否有空餘位置 for (var row = 0; row < 4; row++) { for (var col = 0; col < 4; col++) { if (tiles[row][col] == null) { isOver = false; } } } if (isOver) { // 判斷四周是否有數字相同方塊 for (var row = 0; row < 4; row++) { for (var col = 0; col < 4; col++) { if (row < 3 && tiles[row + 1][col].num == tiles[row][col].num) { isOver = false; } if (row > 0 && tiles[row - 1][col].num == tiles[row][col].num) { isOver = false; } if (col < 3 && tiles[row][col + 1].num == tiles[row][col].num) { isOver = false; } if (col > 0 && tiles[row][col - 1].num == tiles[row][col].num) { isOver = false; } } } } if (isOver) { cc.director.runScene(new cc.TransitionFade(1, new OverScene())); } }
方塊的滑動,就是2048這款遊戲最主要的邏輯了,咱們在滑動的時候,須要遍歷tiles數組全部不是null的元素,並將這個元素按照滑動方向進行移動,一直遍歷到數組的最大長度,也就是將滑動到表格的邊緣,若是中途遇到了方塊,而且數字與本身相同,就進行合併,合併是將本身刪除,將遇到的方塊數字乘以2,若是中途遇到的方塊的數字與本身不一樣,此方塊的滑動就中止,使其停在當前的位置,滑動分爲上下左右四個邏輯,代碼分別以下:
moveUp: function () { var isMoved = false; for (var col = 0; col < 4; col++) { for (var row = 3; row >= 0; row--) { if (tiles[row][col] != null) {// 有方塊 for (var row1 = row; row1 < 3; row1++) { if (tiles[row1 + 1][col] == null)//若是沒有向上移動 { tiles[row1 + 1][col] = tiles[row1][col]; tiles[row1][col] = null; tiles[row1 + 1][col].moveTo(row1 + 1, col); isMoved = true; } else if (tiles[row1 + 1][col].num == tiles[row1][col].num) {// 合併 tiles[row1 + 1][col].num = parseInt(tiles[row1][col].num) * 2; tiles[row1 + 1][col].updateNum(); tiles[row1][col].removeFromParent(); tiles[row1][col] = null; isMoved = true; break; } } } } } return isMoved; }, moveDown: function () { var isMoved = false; for (var col = 0; col < 4; col++) { for (var row = 0; row < 4; row++) { if (tiles[row][col] != null) {// 有方塊 for (var row1 = row; row1 > 0; row1--) { if (tiles[row1 - 1][col] == null)//若是沒有向下移動 { tiles[row1 - 1][col] = tiles[row1][col]; tiles[row1][col] = null; tiles[row1 - 1][col].moveTo(row1 - 1, col); isMoved = true; } else if (tiles[row1 - 1][col].num == tiles[row1][col].num) {// 合併 tiles[row1 - 1][col].num = parseInt(tiles[row1][col].num) * 2; tiles[row1 - 1][col].updateNum(); tiles[row1][col].removeFromParent(); tiles[row1][col] = null; isMoved = true; break; } } } } } return isMoved; }, moveLeft: function () { var isMoved = false; for (var row = 0; row < 4; row++) { for (var col = 0; col < 4; col++) { if (tiles[row][col] != null) { for (var col1 = col; col1 > 0; col1--) { if (tiles[row][col1 - 1] == null) { tiles[row][col1 - 1] = tiles[row][col1]; tiles[row][col1] = null; tiles[row][col1 - 1].moveTo(row, col1 - 1); isMoved = true; } else if (tiles[row][col1 - 1].num == tiles[row][col1].num) {// 合併 tiles[row][col1 - 1].num = parseInt(tiles[row][col1].num) * 2; tiles[row][col1 - 1].updateNum(); tiles[row][col1].removeFromParent(); tiles[row][col1] = null; isMoved = true; break; } } } } } return isMoved; }, moveRight: function () { var isMoved = false; for (var row = 0; row < 4; row++) { for (var col = 3; col >= 0; col--) { if (tiles[row][col] != null) { for (var col1 = col; col1 < 3; col1++) { if (tiles[row][col1 + 1] == null) { tiles[row][col1 + 1] = tiles[row][col1]; tiles[row][col1] = null; tiles[row][col1 + 1].moveTo(row, col1 + 1); isMoved = true; } else if (tiles[row][col1 + 1].num == tiles[row][col1].num) {// 合併 tiles[row][col1 + 1].num = parseInt(tiles[row][col1].num) * 2; tiles[row][col1 + 1].updateNum(); tiles[row][col1].removeFromParent(); tiles[row][col1] = null; isMoved = true; break; } } } } } return isMoved; }
至此,咱們就基本實現了2048遊戲的主要邏輯。
爲了遊戲框架的完整,咱們仍是建立一個開始場景類和結束場景類,代碼分別以下:
開始場景類
var HelloWorldLayer = cc.Layer.extend({ sprite:null, ctor:function () { ////////////////////////////// // 1. super init first this._super(); ///////////////////////////// // 2. add a menu item with "X" image, which is clicked to quit the program // you may modify it. // ask the window size var size = cc.winSize; ///////////////////////////// // 3. add your codes below... // add a label shows "Hello World" // create and initialize a label var helloLabel = new cc.LabelTTF("2048", "Arial", 38); // position the label on the center of the screen helloLabel.x = size.width / 2; helloLabel.y = size.height / 2 + 200; // add the label as a child to this layer this.addChild(helloLabel, 5); // add "HelloWorld" splash screen" // this.sprite = new cc.Sprite(res.HelloWorld_png); // this.sprite.attr({ // x: size.width / 2, // y: size.height / 2 // }); // this.addChild(this.sprite, 0); var start = new cc.MenuItemFont("開始遊戲",function(){ cc.director.runScene(new cc.TransitionFade(1,new GameScene())); }); var menu = new cc.Menu(start); this.addChild(menu); return true; } }); var HelloWorldScene = cc.Scene.extend({ onEnter:function () { this._super(); var layer = new HelloWorldLayer(); this.addChild(layer); } });
結束場景類
/** * Created by Henry on 16/6/19. */ var OverLayer = cc.Layer.extend({ ctor: function () { this._super(); var overText = new cc.LabelTTF("Game Over", "", 50); overText.setPosition(cc.winSize.width / 2, cc.winSize.height / 2); var back = new cc.MenuItemFont("再來一次", function () { cc.director.runScene(new cc.TransitionFade(1, new HelloWorldScene())); }, this); var menu = new cc.Menu(back); this.addChild(menu); return true; } }); var OverScene = cc.Scene.extend({ ctor: function () { this._super(); var layer = new OverLayer(); this.addChild(layer); return true; } });
最後的運行效果以下
經過CVP平臺的項目託管可看到實際運行效果,地址以下:
http://www.cocoscvp.com/usercode/ea72822aeed0546b537b4226954a11be87a7f152/
全部源代碼均上傳到github,歡迎交流學習,地址:
https://github.com/hjcenry/2048