Cocos2d-JS实现的2048
发表于2016-06-21
前言
2048是之前火过一段时间的休闲数字消除类游戏,它的玩法很简单,上手很容易,可是想到要得到高分却很难,看似简单的游戏却有着很多得分的技巧,想当初这个游戏也曾是陪伴我大学课堂的游戏之一。虽然在得分上有着很多的技巧,但对于开发来说,这其实是一件相当容易的事情,仔细分析之后就可能大概理清楚这种消除游戏的逻辑。
游戏分析
这款游戏仔细想想就差不多清楚它的大致的思路,游戏中只有方块这一个我们操作的对象,这个对象包含了所在行,所在列,以及方块显示的数字三个属性,这三个属性足以表达游戏中的所有效果。除了方块,其他的就是游戏中必不可少的背景图层,开始及结束场景等。由于游戏中需要将4x4的方块们整齐排列,因此还需要一个四行四列的表格,来呈现我们的游戏效果。
滑动逻辑
游戏中最主要的操作就是通过手指触摸屏幕进行滑屏操作,带动场景中的方块整体移动,并且遇到相同数字的方块进行合并。滑动的逻辑就是遍历场景中所有的方块,每一个方块在滑动方向进行移动,如果前方没有方块,方块就一直滑动,如果前方有方块,判断自己的数字与这个方块的数字是否相同,相同进行合并操作,不相同则停在当前位置。
游戏实现
在对游戏逻辑进行了分析之后,就可以用代码进行实现了,编码,其实就是一个将游戏逻辑转换为机器语言的过程而已。
方块类
首先,我们需要一个类来存储方块的长度和宽度,代码如下:
1 2 3 4 5 | // 保存方块长度方块 var tile = { width: 0, height: 0 }; |
1 2 3 4 5 6 7 8 9 10 | var Tiled = cc.Node.extend({ num: 0, col: 0, row: 0, ctor: function (num) { this ._super(); return true ; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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 ; } } |
在updateNum方法中,我们主要做两件事,更新方块显示数字和更新方块背景颜色(在方块的背景色上,我还专门上网搜了几种颜色搭配,恩,感觉很有艺术美,哈哈),代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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 ; } } |
1 2 3 4 5 6 7 8 9 10 11 12 | 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的表格中的方块的信息。
有了以上四个属性之后,就可以在构造函数中进行初始化了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 ; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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())); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | 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游戏的主要逻辑。
开始/结束场景类
为了游戏框架的完整,我们还是创建一个开始场景类和结束场景类,代码分别如下:
开始场景类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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); } }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * 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
原文博客:http://hjcenry.github.io
看了上面的文章 热爱游戏创作的你是不是已经开始热血沸腾了呢?是不是迫不及待的想加入游戏团队成为里面的一员呢?
福利来啦~赶快加入腾讯GAD交流群,人满封群!每天分享游戏开发内部干货、教学视频、福利活动、和有相同梦想的人在一起,更有腾讯游戏专家手把手教你做游戏!
