zoukankan      html  css  js  c++  java
  • 用Cocos2d-JS制作类神经猫的游戏《你是我的小羊驼》 转载

    (via:Cocos 引擎中文站

    一夜之间,微信上一款叫《围住神经猫》的小游戏火了。它的玩法很简单,用最少的步数把一只神经兮兮的猫围死。 7月22号上线以来,3天、500万用户和1亿访问,想必各位程序猿都按耐不住了,想实现自己的神经猫游戏。

    在这篇教程里,我会教大家如何用Cocos2d-JS来实现一个神经猫这样的游戏。 让我们先看下游戏最后完成了的效果图:

    你可能注意到了,神经猫换成了可爱的小羊驼:)

    在线游戏地址:http://app9.download.anzhuoshangdian.com/xyt/?from=singlemessage&isappinstalled=0

    源码地址:https://github.com/chukong/cocos-docs/blob/master/tutorial/framework/html5/how-to-make-a-cat-game/res/Catnorris-master.zip

    游戏分析

    三个界面基本上就是整个游戏的全部内容:

    1.左边的是主界面,展示游戏名称以及主角,让玩家对游戏的整体画风有个大概的印象。

    2.中间的是游戏界面,点击空格防止橙色六边形砖块来围堵小羊驼。

    3.右边的是游戏成功或失败的界面。

    整个游戏的主逻辑都在游戏界面中完成。

    玩法是这样:

    1.游戏初始化开始,小羊驼始终是站在地图中间,在地图的其他区域随机生产一些位置随机的砖块。

    2.玩家点击一个空白区域,放置一个砖块来围堵羊驼。

    3.羊驼AI寻路移动一步。

    4.循环2和3,直到羊驼被围堵在一个圈里面(游戏成功),或羊驼到达地图边界(游戏失败)

    整个游戏的思路理清楚了,接下来我们开始进入编码阶段。

    开发环境与新建项目

    本教程开发基于当前最新的Download v3.0RC1.  (其他版本下载地址:http://cn.cocos2d-x.org/download/

    下载引擎并解压到磁盘的某个目录。

    打开控制台,输入下面的命令来新建项目。

    1. $cd cocos2d-js-v3.0-rc1/tools/cocos2d-console/bin 
    2. $./cocos new -l js --no-native 
    3. $cd MyJSGame/ 
    4. $../cocos run -p web 

    环境搭建并不是这篇文章的重点,更详细的信息可以参考:《搭建 Cocos2d-JS 开发环境

    主界面实现

    游戏的入口代码在main.js中,用编辑器打开并修改为下面的代码。

    1. cc.game.onStart = function(){ 
    2.     // 1. 
    3.     cc.view.adjustViewPort(true); 
    4.   
    5.     // 2. 
    6.     if (cc.sys.isMobile) 
    7.         cc.view.setDesignResolutionSize(320,500,cc.ResolutionPolicy.FIXED_WIDTH); 
    8.     else cc.view.setDesignResolutionSize(320,480,cc.ResolutionPolicy.SHOW_ALL); 
    9.     cc.view.resizeWithBrowserSize(true); 
    10.   
    11.     // 3. 
    12.     cc.LoaderScene.preload(resources, function () { 
    13.         // 4. 
    14.         gameScene = new GameScene(); 
    15.         cc.director.runScene(gameScene); 
    16.     }, this); 
    17. }; 
    18.   
    19. cc.game.run(); 

    关键点解析如下:

    1.设置浏览器meta来适配屏幕,引擎内部会根据屏幕大小来设置meta的viewport值,会达到更好的屏幕适配效果。

    2.针对手机浏览器和PC浏览器启用不同的分辨率适配策略。

    3.预加载图片声音等资源。 cc.LoaderScene.preload会生成一个“加载中 x%”的界面,等待资源加载结束后,调用第二个参数传入的匿名函数。 对于基于html的游戏,页面是放在服务器端供浏览器下载的,为了获得流畅的用户体验,cc.LoaderScene.preload让浏览器先把远程服 务器的资源缓存到本地。需要预加载的资源定义在src/Resources.js文件中。

    4.启动游戏的第一个场景。

    主界面的由两个层实现:

    1.GameLayer层,游戏主逻辑层,在未初始化地图矩阵时,它只显示背景地图。

    2.StartUI层,显示logo图片和开始游戏按钮。

    GameScene的初始化代码如下:

    1. var GameScene = cc.Scene.extend({ 
    2.     onEnter : function () { 
    3.         this._super(); 
    4.   
    5.         var bg = new cc.Sprite(res.bg); 
    6.         bg.attr({ 
    7.             anchorX : 0.5, 
    8.             anchorY : 0.5, 
    9.             x : cc.winSize.width/2, 
    10.             y : cc.winSize.height/2 
    11.         }); 
    12.         this.addChild(bg); 
    13.   
    14.         layers.game = new GameLayer(); 
    15.         this.addChild(layers.game); 
    16.   
    17.         layers.startUI = new StartUI(); 
    18.         this.addChild(layers.startUI); 
    19.   
    20.         layers.winUI = new ResultUI(true); 
    21.         layers.loseUI = new ResultUI(false); 
    22.         layers.shareUI = new ShareUI(); 
    23.     } 
    24. }); 

    由引擎提供的cc.Scene.extend方法,让js能实现高级面向对象语言的继承特性。 onEnter方法是场景初始化完成即将展示的消息回调,在onEnter中必须调用this._super();来确保Scene被正确的初始化。

    整个游戏的设计只有一个scene,界面之间的切换由layer来实现,这可能不是一个最优的设计,但也提供另一种思路。 为了用layer来实现切换,全局变量layers存储了各层的一个实例。

    GameLayer我们在下一章节中详细讲解。

    StartUI的实现如下:

    1. var StartUI = cc.Layer.extend({ 
    2.     ctor : function () { 
    3.         this._super(); 
    4.   
    5.         var start = new cc.Sprite(res.start); 
    6.         start.x = cc.winSize.width/2; 
    7.         start.y = cc.winSize.height/2 + 20; 
    8.         this.addChild(start); 
    9.     }, 
    10.     onEnter : function () { 
    11.         this._super(); 
    12.   
    13.         cc.eventManager.addListener({ 
    14.             event: cc.EventListener.TOUCH_ALL_AT_ONCE, 
    15.             onTouchesEnded: function (touches, event) { 
    16.                 var touch = touches[0]; 
    17.                 var pos = touch.getLocation(); 
    18.                 if (pos.y < cc.winSize.height/3) { 
    19.                     layers.game.initGame(); 
    20.                     layers.startUI.removeFromParent(); 
    21.                 } 
    22.             } 
    23.         }, this); 
    24.     } 
    25. }); 

    cc.Layer.extend作用同cc.Scene.extend一样,只不过是一个扩展Scene,一个扩展Layer。ctor是Cocos2d-JS中的构造函数,在ctor中必须调用this._super();以确保正确的初始化。

    在onEnter中,我们为StartUI层绑定事件监听,判断触摸点的位置坐标来触发scene切换。

    细心的读者可能要问,为什么不用Menu控件? 当前的Cocos2d-JS版本已实现模块化,可以选择只加载游戏中用到的模块,已减少最终打包size。 为了不加入Menu模块,这里使用了最简单的触摸点坐标判断来实现通用的事情。

    游戏界面的实现

    橙色块的初始化

    游戏地图区域是由9*9的六边形方块组成的,首先用InActive的图片初始化一边矩阵。相关代码如下:

    1. var ox = x = y = 0, odd = false, block, tex = this.batch.texture; 
    2. for (var r = 0; r < ROW; r++) { 
    3.     y = BLOCK_YREGION * r; 
    4.     ox = odd * OFFSET_ODD; 
    5.     for (var c = 0; c < COL; c++) { 
    6.         x = ox + BLOCK_XREGION * c; 
    7.         block = new cc.Sprite(tex, BLOCK2_RECT); 
    8.         block.attr({ 
    9.             anchorX : 0, 
    10.             anchorY : 0, 
    11.             x : x, 
    12.             y : y, 
    13.             width : BLOCK_W, 
    14.             height : BLOCK_H 
    15.         }); 
    16.         this.batch.addChild(block); 
    17.     } 
    18.     odd = !odd; 

    每次循环odd改变,已实现上下错位的排布。 attr是Node基类的新方法,可以方便的一次性设置多个属性。

    橙色方块的初始化是由initGame函数来完成。 先来看initGame的实现:

    1. initGame : function() { 
    2.     if (this.inited) return; 
    3.   
    4.     this.player_c = this.player_r = 4; 
    5.     this.step = 0; 
    6.   
    7.     // 1. 
    8.     for (var i = 0, l = this.active_nodes.length; i < l; i++) { 
    9.         this.active_nodes[i].removeFromParent(); 
    10.     } 
    11.     this.active_nodes = []; 
    12.     for (var r = 0; r < ROW; r++) { 
    13.         for (var c = 0; c < COL; c++) { 
    14.             this.active_blocks[r][c] = false; 
    15.         } 
    16.     } 
    17.   
    18.     // 2. 
    19.     this.randomBlocks(); 
    20.   
    21.     // 3. 
    22.     this.player.attr({ 
    23.         anchorX : 0.5, 
    24.         anchorY : 0, 
    25.         x : OFFSET_X + BLOCK_XREGION * this.player_c + BLOCK_W/2, 
    26.         y : OFFSET_Y + BLOCK_YREGION * this.player_r - 5 
    27.     }); 
    28.     this.player.stopAllActions(); 
    29.     this.player.runAction(this.moving_action); 
    30.   
    31.     this.inited = true; 
    32. }, 

    要点解析如下:

    1.为了方便逻辑处理,这里用了active_nodes和active_blocks来记录被激活的方块。在初始化矩阵前,需要清理上一次游戏已生成的橙色方块。active_nodes存储精灵实例,active_blocks记录精灵的矩阵坐标。

    2.randomBlocks函数生成随机橙色砖块。 首先产生一个7-20的随机数,也就是确定橙色块的数量。然后循环确定每一个块的位置坐标,当然位置坐标也是随机确定的。

    3.复位小羊驼的位置以及动画。

    响应触摸事件

    按照我们之前的分析,游戏界面初始化完成后,需要等待用户指令才能进行下一步的游戏。

    相关代码如下:

    1. // 1. 
    2. cc.eventManager.addListener({ 
    3.     // 2. 
    4.     event: cc.EventListener.TOUCH_ALL_AT_ONCE, 
    5.     // 3. 
    6.     onTouchesBegan: function (touches, event) { 
    7.         var touch = touches[0]; 
    8.         var pos = touch.getLocation(); 
    9.         var target = event.getCurrentTarget(); 
    10.         if (!target.inited) return; 
    11.   
    12.         pos.y -= OFFSET_Y; 
    13.         var r = Math.floor(pos.y / BLOCK_YREGION); 
    14.         pos.x -= OFFSET_X + (r%2==1) * OFFSET_ODD; 
    15.         var c = Math.floor(pos.x / BLOCK_XREGION); 
    16.         if (c >= 0 && r >= 0 && c < COL && r < ROW) { 
    17.             if (target.activateBlock(r, c)) { 
    18.                 target.step ++; 
    19.                 target.movePlayer(); 
    20.             } 
    21.         } 
    22.     } 
    23. }, this); 

    1. cc.eventManager.addListener加入新的事件监听。

    2. 设置事件监听模式为TOUCH_ALL_AT_ONCE。

    3. 重写onTouchesBegan方法,判断触摸点的坐标,确定是哪个块被点击,并做响应的处理。 activateBlock方法在对应的矩阵位置加入橙色块,并更新状态数组。然后调用movePlayer移动小羊驼。

    羊驼的移动

    整个逻辑的关键是AI.js中的getDistance函数,

    getDistance有6个参数:

    1.羊驼所在行号

    2.羊驼所在列号

    3.前进方向,l_choices、r_choices、t_choices或b_choices

    4.激活块的记录数组

    5.辅助记录表,记录在寻路算法中某个节点是不是已经被访问过。

    6.最短路径

    返回值有三种情况:

    1.羊驼到达地图边界,返回羊驼坐标和最短路径0

    2.羊驼还在地图中,返回羊驼的下一个坐标值和最短路径cost

    3. -1表示羊驼被圈住了,但可能可以移动。

    getDistance的代码实现如下:

    1. var getDistance = function (r, c, dir_choices, activate_blocs, passed, cost) { 
    2.     passed[r][c] = true; 
    3.     if (r <= 0 || r >= ROW_MINUS_1 || c <= 0 || c >= COL_MINUS_1) { 
    4.         return [r, c, cost]; 
    5.     } 
    6.   
    7.     var odd = (r % 2 == 1) ? 1 : 0; 
    8.     var choices = dir_choices[odd]; 
    9.   
    10.     var nextr, nextc, result; 
    11.     for (var i = 0, l = choices.length; i < l; i++) { 
    12.         nextr = r + choices[i][0]; 
    13.         nextc = c + choices[i][4]; 
    14.   
    15.         if (!activate_blocs[nextr][nextc] && !passed[nextr][nextc]) { 
    16.             cost ++; 
    17.             result = getDistance(nextr, nextc, dir_choices, activate_blocs, passed, cost); 
    18.             if (result != -1) { 
    19.                 result[0] = nextr; 
    20.                 result[1] = nextc; 
    21.                 return result; 
    22.             } 
    23.         } 
    24.     } 
    25.     return -1; 
    26. }; 

    在羊驼移动函数movePlayer中,首先通过getDistance来判断上下左右4个方向,来寻找最佳移动方向。根据getDistance的返回结果做相应的逻辑处理。

    游戏结束界面

    游戏结束的两种情况,玩家胜利或失败。

    在ResultUI的构造函数中,加入参数win,用来标识是否胜利。而胜利和失败仅仅是显示文字的区别,下方的两个按钮均一样。

    在ctor中,根据是否胜利加载不同的图片来显示:

    1. ctor : function (win) { 
    2.      this._super(); 
    3.   
    4.      this.win = win; 
    5.      if (win) { 
    6.          this.winPanel = new cc.Sprite(res.succeed); 
    7.          this.winPanel.x = cc.winSize.width/2; 
    8.          this.winPanel.anchorY = 0.2; 
    9.          this.winPanel.y = cc.winSize.height/2; 
    10.          this.addChild(this.winPanel); 
    11.      } 
    12.      else { 
    13.          this.losePanel = new cc.Sprite(res.failed); 
    14.          this.losePanel.x = cc.winSize.width/2; 
    15.          this.losePanel.anchorY = 0.2; 
    16.          this.losePanel.y = cc.winSize.height/2; 
    17.          this.addChild(this.losePanel); 
    18.      } 
    19.  } 

    在onEnter中,根据是否胜利加载不同的文字描述:

    1. if (this.win) { 
    2.     this.winPanel.removeAllChildren(); 
    3.   
    4.     var w = this.winPanel.width, h = this.winPanel.height; 
    5.     var label = new cc.LabelTTF("继续刷屏! "+step+"步推倒我的小羊驼 打败"+percent+"%朋友圈的人! 你能超过我吗?", "宋体", 20); 
    6.     label.x = w/2; 
    7.     label.y = h/4; 
    8.     label.textAlign = cc.LabelTTF.TEXT_ALIGNMENT_CENTER; 
    9.     //label.boundingWidth = w; 
    10.     label.width = w; 
    11.     label.color = cc.color(0, 0, 0); 
    12.     this.winPanel.addChild(label); 
    13. else { 
    14.     this.losePanel.removeAllChildren(); 
    15.     var w = this.losePanel.width, h = this.losePanel.height; 
    16.     label = new cc.LabelTTF("我滴小羊驼呀它又跑掉了 T_T 快帮我抓回来!", "宋体", 20); 
    17.     label.x = w/2; 
    18.     label.y = h/4+5; 
    19.     label.textAlign = cc.LabelTTF.TEXT_ALIGNMENT_CENTER; 
    20.     //label.boundingWidth = w; 
    21.     label.width = w; 
    22.     label.color = cc.color(0, 0, 0); 
    23.     this.losePanel.addChild(label, 10); 

    "通知好友"按钮加载shareUI层,这个层其实是一个帮助指导界面,指示用户点击微信右上角的分享按钮进行分享。

    1. gameScene.addChild(layers.shareUI, 100); 
    2. target.win ? share(1, step, percent) : share(2); 

    "再来一次"实现很简单,调用initGame重新初始化矩阵,并移除ResultUI层。

    1. layers.game.initGame(); 
    2. target.win ? layers.winUI.removeFromParent() : layers.loseUI.removeFromParent(); 

    分享指导界面

    在游戏结束界面我们加入了分享按钮。现在我们就来实现分享界面。

    分享界面由分享图标和分享说明组成。这和前面的layer创建一样。很简单,唯一的区别是,分享界面是cc.LayerColor(cc.LayerColor支持设置层的颜色)的子类。下面是实现代码:

    1. ctor: function () { 
    2.     this._super(cc.color(0, 0, 0, 188), cc.winSize.width, cc.winSize.height); 
    3.   
    4.     var arrow = new cc.Sprite(res.arrow); 
    5.     arrow.anchorX = 1; 
    6.     arrow.anchorY = 1; 
    7.     arrow.x = cc.winSize.width - 15; 
    8.     arrow.y = cc.winSize.height - 5; 
    9.     this.addChild(arrow); 
    10.   
    11.     var label = new cc.LabelTTF("请点击右上角的菜单按钮 再点"分享到朋友圈" 让好友们挑战你的分数!", "宋体", 20, cc.size(cc.winSize.width*0.7, 250), cc.TEXT_ALIGNMENT_CENTER); 
    12.     label.x = cc.winSize.width/2; 
    13.     label.y = cc.winSize.height - 100; 
    14.     label.anchorY = 1; 
    15.     this.addChild(label); 
    16. }, 

    加入touch事件用于移除分享界面:

    1. onEnter: function () { 
    2.     this._super(); 
    3.     cc.eventManager.addListener({ 
    4.         event: cc.EventListener.TOUCH_ONE_BY_ONE, 
    5.         onTouchBegan: function (touch, event) { 
    6.             layers.shareUI.removeFromParent(); 
    7.         } 
    8.     }, this); 

    微信分享

    我们需要的功能:

    1.分享到微信朋友圈

    2.分享给微信好友

    3.分享到腾讯微博

    4.关注指定用户

    实现方式

    本功能已经有大神提供了完整的库,地址是:https://github.com/zxlie/WeixinApi ,以下我们做一个简单的使用分析。

    注:除特殊说明外,本小节实现均在文件 WeixinApi.js中。

    现在我们实现的分享有,发给指定朋友,分享到朋友圈,分享到腾讯微博。对于不同的分享方式,实现方式大同小异,我们主要以分享到朋友圈为例。

    分享数据

    我们分享的时候需要的数据有:appid,图片,链接,标题,文字内容,例如:

    对应在代码中就需要以下数据:

    1. "appid":theData.appId ? theData.appId : '', 
    2. "img_url":theData.imgUrl, 
    3. "link":theData.link, 
    4. "desc":theData.desc, 
    5. "title":theData.title, // 注意这里要分享出去的内容是desc 

    数据来源

    为了得到数据,我们需要在GameScene.js中实现ResultUI的时候,将以上数据生成出来。 比如胜利时,我们需要显示:

    1. var label = new cc.LabelTTF("继续刷屏! "+step+"步推倒我的小羊驼 打败"+percent+"%朋友圈的人! 你能超过我吗?", "宋体", 20); 

    完成数据后,我们需要判断胜利或失败,并传回ui中显示:

    1. target.win ? share(1, step, percent) : share(2); 

    分享回调

    为了监测分享的状态,无论分享成功与否我们回调都会上报状态,以便程序处理,我们需要的状态有:

    1.用户取消分享

    2.分享失败

    3.分享成功 所以我们需要以下实现:

    1. WeixinJSBridge.on('menu:share:timeline', function (argv) { 
    2.     if (callbacks.async && callbacks.ready) { 
    3.         window["_wx_loadedCb_"] = callbacks.dataLoaded || new Function(); 
    4.         if(window["_wx_loadedCb_"].toString().indexOf("_wx_loadedCb_") > 0) { 
    5.             window["_wx_loadedCb_"] = new Function(); 
    6.         } 
    7.         callbacks.dataLoaded = function (newData) { 
    8.             window["_wx_loadedCb_"](newData); 
    9.             shareTimeline(newData); 
    10.         }; 
    11.         // 然后就绪 
    12.         callbacks.ready && callbacks.ready(argv); 
    13.     } else { 
    14.         // 就绪状态 
    15.         callbacks.ready && callbacks.ready(argv); 
    16.         shareTimeline(data); 
    17.     } 
    18. }); 

    WeixinJSBridge

    在微信上,通过公众平台推送给用户的文章,是在微信内部直接打开的,用的无外乎就是一个UIWebView控件(IOS上,Android上也 差不多)。但特殊的是,微信官方在这里面加了一个默认的Js API--WeixinJSBridge,通过它,能直接在该页面上做分享操作。 以下代码,拿去玩吧:

    1. WeixinJSBridge.on('menu:share:timeline', function (argv) { 
    2.     if (callbacks.async && callbacks.ready) { 
    3.         window["_wx_loadedCb_"] = callbacks.dataLoaded || new Function(); 
    4.         if(window["_wx_loadedCb_"].toString().indexOf("_wx_loadedCb_") > 0) { 
    5.             window["_wx_loadedCb_"] = new Function(); 
    6.         } 
    7.         callbacks.dataLoaded = function (newData) { 
    8.             window["_wx_loadedCb_"](newData); 
    9.             shareTimeline(newData); 
    10.         }; 
    11.         // 然后就绪 
    12.         callbacks.ready && callbacks.ready(argv); 
    13.     } else { 
    14.         // 就绪状态 
    15.         callbacks.ready && callbacks.ready(argv); 
    16.         shareTimeline(data); 
    17.     } 
    18. }); 

    最后,赶紧写点诱惑的东东,让用户分享出去吧,这是微信病毒传播的乐趣!

  • 相关阅读:
    gThumb 3.1.2 发布,支持 WebP 图像
    航空例行天气预报解析 metaf2xml
    Baruwa 1.1.2 发布,邮件监控系统
    Bisect 1.3 发布,Caml 代码覆盖测试
    MoonScript 0.2.2 发布,基于 Lua 的脚本语言
    Varnish 入门
    快速增量备份程序 DeltaCopy
    恢复模糊的图像 SmartDeblur
    Cairo 1.12.8 发布,向量图形会图库
    iText 5.3.4 发布,Java 的 PDF 开发包
  • 原文地址:https://www.cnblogs.com/playerboy/p/3957097.html
Copyright © 2011-2022 走看看