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. }); 

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

  • 相关阅读:
    css边框以及其他常用样式
    jemeter学习-badboy录制与代理服务器录制
    linux基础命令学习
    cookie与session
    网址保存
    安全性测试要点转摘
    Charles弱网测试转载
    java 中文乱码以及转码
    spring学习总结——高级装配学习四(运行时:值注入、spring表达式)
    spring学习总结——高级装配学习三(Bean的作用域)
  • 原文地址:https://www.cnblogs.com/playerboy/p/3957097.html
Copyright © 2011-2022 走看看