用Cocos2d-JS制作類神經(jīng)貓的游戲《你是我的小羊駝》
(via:Cocos 引擎中文站)
一夜之間,微信上一款叫《圍住神經(jīng)貓》的小游戲火了。它的玩法很簡(jiǎn)單,用最少的步數(shù)把一只神經(jīng)兮兮的貓圍死。 7月22號(hào)上線以來,3天、500萬用戶和1億訪問,想必各位程序猿都按耐不住了,想實(shí)現(xiàn)自己的神經(jīng)貓游戲。
在這篇教程里,我會(huì)教大家如何用Cocos2d-JS來實(shí)現(xiàn)一個(gè)神經(jīng)貓這樣的游戲。 讓我們先看下游戲***完成了的效果圖:
你可能注意到了,神經(jīng)貓換成了可愛的小羊駝:)
在線游戲地址:http://app9.download.anzhuoshangdian.com/xyt/?from=singlemessage&isappinstalled=0
游戲分析
三個(gè)界面基本上就是整個(gè)游戲的全部?jī)?nèi)容:
1.左邊的是主界面,展示游戲名稱以及主角,讓玩家對(duì)游戲的整體畫風(fēng)有個(gè)大概的印象。
2.中間的是游戲界面,點(diǎn)擊空格防止橙色六邊形磚塊來圍堵小羊駝。
3.右邊的是游戲成功或失敗的界面。
整個(gè)游戲的主邏輯都在游戲界面中完成。
玩法是這樣:
1.游戲初始化開始,小羊駝始終是站在地圖中間,在地圖的其他區(qū)域隨機(jī)生產(chǎn)一些位置隨機(jī)的磚塊。
2.玩家點(diǎn)擊一個(gè)空白區(qū)域,放置一個(gè)磚塊來圍堵羊駝。
3.羊駝AI尋路移動(dòng)一步。
4.循環(huán)2和3,直到羊駝被圍堵在一個(gè)圈里面(游戲成功),或羊駝到達(dá)地圖邊界(游戲失?。?/p>
整個(gè)游戲的思路理清楚了,接下來我們開始進(jìn)入編碼階段。
開發(fā)環(huán)境與新建項(xiàng)目
本教程開發(fā)基于當(dāng)前***的Download v3.0RC1. (其他版本下載地址:http://cn.cocos2d-x.org/download/)
下載引擎并解壓到磁盤的某個(gè)目錄。
打開控制臺(tái),輸入下面的命令來新建項(xiàng)目。
- $cd cocos2d-js-v3.0-rc1/tools/cocos2d-console/bin
- $./cocos new -l js --no-native
- $cd MyJSGame/
- $../cocos run -p web
環(huán)境搭建并不是這篇文章的重點(diǎn),更詳細(xì)的信息可以參考:《搭建 Cocos2d-JS 開發(fā)環(huán)境》
主界面實(shí)現(xiàn)
游戲的入口代碼在main.js中,用編輯器打開并修改為下面的代碼。
- cc.game.onStart = function(){
- // 1.
- cc.view.adjustViewPort(true);
- // 2.
- if (cc.sys.isMobile)
- cc.view.setDesignResolutionSize(320,500,cc.ResolutionPolicy.FIXED_WIDTH);
- else cc.view.setDesignResolutionSize(320,480,cc.ResolutionPolicy.SHOW_ALL);
- cc.view.resizeWithBrowserSize(true);
- // 3.
- cc.LoaderScene.preload(resources, function () {
- // 4.
- gameScene = new GameScene();
- cc.director.runScene(gameScene);
- }, this);
- };
- cc.game.run();
關(guān)鍵點(diǎn)解析如下:
1.設(shè)置瀏覽器meta來適配屏幕,引擎內(nèi)部會(huì)根據(jù)屏幕大小來設(shè)置meta的viewport值,會(huì)達(dá)到更好的屏幕適配效果。
2.針對(duì)手機(jī)瀏覽器和PC瀏覽器啟用不同的分辨率適配策略。
3.預(yù)加載圖片聲音等資源。 cc.LoaderScene.preload會(huì)生成一個(gè)“加載中 x%”的界面,等待資源加載結(jié)束后,調(diào)用第二個(gè)參數(shù)傳入的匿名函數(shù)。 對(duì)于基于html的游戲,頁面是放在服務(wù)器端供瀏覽器下載的,為了獲得流暢的用戶體驗(yàn),cc.LoaderScene.preload讓瀏覽器先把遠(yuǎn)程服 務(wù)器的資源緩存到本地。需要預(yù)加載的資源定義在src/Resources.js文件中。
4.啟動(dòng)游戲的***個(gè)場(chǎng)景。
主界面的由兩個(gè)層實(shí)現(xiàn):
1.GameLayer層,游戲主邏輯層,在未初始化地圖矩陣時(shí),它只顯示背景地圖。
2.StartUI層,顯示logo圖片和開始游戲按鈕。
GameScene的初始化代碼如下:
- var GameScene = cc.Scene.extend({
- onEnter : function () {
- this._super();
- var bg = new cc.Sprite(res.bg);
- bg.attr({
- anchorX : 0.5,
- anchorY : 0.5,
- x : cc.winSize.width/2,
- y : cc.winSize.height/2
- });
- this.addChild(bg);
- layers.game = new GameLayer();
- this.addChild(layers.game);
- layers.startUI = new StartUI();
- this.addChild(layers.startUI);
- layers.winUI = new ResultUI(true);
- layers.loseUI = new ResultUI(false);
- layers.shareUI = new ShareUI();
- }
- });
由引擎提供的cc.Scene.extend方法,讓js能實(shí)現(xiàn)高級(jí)面向?qū)ο笳Z言的繼承特性。 onEnter方法是場(chǎng)景初始化完成即將展示的消息回調(diào),在onEnter中必須調(diào)用this._super();來確保Scene被正確的初始化。
整個(gè)游戲的設(shè)計(jì)只有一個(gè)scene,界面之間的切換由layer來實(shí)現(xiàn),這可能不是一個(gè)***的設(shè)計(jì),但也提供另一種思路。 為了用layer來實(shí)現(xiàn)切換,全局變量layers存儲(chǔ)了各層的一個(gè)實(shí)例。
GameLayer我們?cè)谙乱徽鹿?jié)中詳細(xì)講解。
StartUI的實(shí)現(xiàn)如下:
- var StartUI = cc.Layer.extend({
- ctor : function () {
- this._super();
- var start = new cc.Sprite(res.start);
- start.x = cc.winSize.width/2;
- start.y = cc.winSize.height/2 + 20;
- this.addChild(start);
- },
- onEnter : function () {
- this._super();
- cc.eventManager.addListener({
- event: cc.EventListener.TOUCH_ALL_AT_ONCE,
- onTouchesEnded: function (touches, event) {
- var touch = touches[0];
- var pos = touch.getLocation();
- if (pos.y < cc.winSize.height/3) {
- layers.game.initGame();
- layers.startUI.removeFromParent();
- }
- }
- }, this);
- }
- });
cc.Layer.extend作用同cc.Scene.extend一樣,只不過是一個(gè)擴(kuò)展Scene,一個(gè)擴(kuò)展Layer。ctor是Cocos2d-JS中的構(gòu)造函數(shù),在ctor中必須調(diào)用this._super();以確保正確的初始化。
在onEnter中,我們?yōu)镾tartUI層綁定事件監(jiān)聽,判斷觸摸點(diǎn)的位置坐標(biāo)來觸發(fā)scene切換。
細(xì)心的讀者可能要問,為什么不用Menu控件? 當(dāng)前的Cocos2d-JS版本已實(shí)現(xiàn)模塊化,可以選擇只加載游戲中用到的模塊,已減少最終打包size。 為了不加入Menu模塊,這里使用了最簡(jiǎn)單的觸摸點(diǎn)坐標(biāo)判斷來實(shí)現(xiàn)通用的事情。
游戲界面的實(shí)現(xiàn)
橙色塊的初始化
游戲地圖區(qū)域是由9*9的六邊形方塊組成的,首先用InActive的圖片初始化一邊矩陣。相關(guān)代碼如下:
- var ox = x = y = 0, odd = false, block, tex = this.batch.texture;
- for (var r = 0; r < ROW; r++) {
- y = BLOCK_YREGION * r;
- ox = odd * OFFSET_ODD;
- for (var c = 0; c < COL; c++) {
- x = ox + BLOCK_XREGION * c;
- block = new cc.Sprite(tex, BLOCK2_RECT);
- block.attr({
- anchorX : 0,
- anchorY : 0,
- x : x,
- y : y,
- width : BLOCK_W,
- height : BLOCK_H
- });
- this.batch.addChild(block);
- }
- odd = !odd;
- }
每次循環(huán)odd改變,已實(shí)現(xiàn)上下錯(cuò)位的排布。 attr是Node基類的新方法,可以方便的一次性設(shè)置多個(gè)屬性。
橙色方塊的初始化是由initGame函數(shù)來完成。 先來看initGame的實(shí)現(xiàn):
- initGame : function() {
- if (this.inited) return;
- this.player_c = this.player_r = 4;
- this.step = 0;
- // 1.
- for (var i = 0, l = this.active_nodes.length; i < l; i++) {
- this.active_nodes[i].removeFromParent();
- }
- this.active_nodes = [];
- for (var r = 0; r < ROW; r++) {
- for (var c = 0; c < COL; c++) {
- this.active_blocks[r][c] = false;
- }
- }
- // 2.
- this.randomBlocks();
- // 3.
- this.player.attr({
- anchorX : 0.5,
- anchorY : 0,
- x : OFFSET_X + BLOCK_XREGION * this.player_c + BLOCK_W/2,
- y : OFFSET_Y + BLOCK_YREGION * this.player_r - 5
- });
- this.player.stopAllActions();
- this.player.runAction(this.moving_action);
- this.inited = true;
- },
要點(diǎn)解析如下:
1.為了方便邏輯處理,這里用了active_nodes和active_blocks來記錄被激活的方塊。在初始化矩陣前,需要清理上一次游戲已生成的橙色方塊。active_nodes存儲(chǔ)精靈實(shí)例,active_blocks記錄精靈的矩陣坐標(biāo)。
2.randomBlocks函數(shù)生成隨機(jī)橙色磚塊。 首先產(chǎn)生一個(gè)7-20的隨機(jī)數(shù),也就是確定橙色塊的數(shù)量。然后循環(huán)確定每一個(gè)塊的位置坐標(biāo),當(dāng)然位置坐標(biāo)也是隨機(jī)確定的。
3.復(fù)位小羊駝的位置以及動(dòng)畫。
響應(yīng)觸摸事件
按照我們之前的分析,游戲界面初始化完成后,需要等待用戶指令才能進(jìn)行下一步的游戲。
相關(guān)代碼如下:
- // 1.
- cc.eventManager.addListener({
- // 2.
- event: cc.EventListener.TOUCH_ALL_AT_ONCE,
- // 3.
- onTouchesBegan: function (touches, event) {
- var touch = touches[0];
- var pos = touch.getLocation();
- var target = event.getCurrentTarget();
- if (!target.inited) return;
- pos.y -= OFFSET_Y;
- var r = Math.floor(pos.y / BLOCK_YREGION);
- pos.x -= OFFSET_X + (r%2==1) * OFFSET_ODD;
- var c = Math.floor(pos.x / BLOCK_XREGION);
- if (c >= 0 && r >= 0 && c < COL && r < ROW) {
- if (target.activateBlock(r, c)) {
- target.step ++;
- target.movePlayer();
- }
- }
- }
- }, this);
1. cc.eventManager.addListener加入新的事件監(jiān)聽。
2. 設(shè)置事件監(jiān)聽模式為TOUCH_ALL_AT_ONCE。
3. 重寫onTouchesBegan方法,判斷觸摸點(diǎn)的坐標(biāo),確定是哪個(gè)塊被點(diǎn)擊,并做響應(yīng)的處理。 activateBlock方法在對(duì)應(yīng)的矩陣位置加入橙色塊,并更新狀態(tài)數(shù)組。然后調(diào)用movePlayer移動(dòng)小羊駝。
羊駝的移動(dòng)
整個(gè)邏輯的關(guān)鍵是AI.js中的getDistance函數(shù),
getDistance有6個(gè)參數(shù):
1.羊駝所在行號(hào)
2.羊駝所在列號(hào)
3.前進(jìn)方向,l_choices、r_choices、t_choices或b_choices
4.激活塊的記錄數(shù)組
5.輔助記錄表,記錄在尋路算法中某個(gè)節(jié)點(diǎn)是不是已經(jīng)被訪問過。
6.最短路徑
返回值有三種情況:
1.羊駝到達(dá)地圖邊界,返回羊駝坐標(biāo)和最短路徑0
2.羊駝還在地圖中,返回羊駝的下一個(gè)坐標(biāo)值和最短路徑cost
3. -1表示羊駝被圈住了,但可能可以移動(dòng)。
getDistance的代碼實(shí)現(xiàn)如下:
- var getDistance = function (r, c, dir_choices, activate_blocs, passed, cost) {
- passed[r][c] = true;
- if (r <= 0 || r >= ROW_MINUS_1 || c <= 0 || c >= COL_MINUS_1) {
- return [r, c, cost];
- }
- var odd = (r % 2 == 1) ? 1 : 0;
- var choices = dir_choices[odd];
- var nextr, nextc, result;
- for (var i = 0, l = choices.length; i < l; i++) {
- nextr = r + choices[i][0];
- nextc = c + choices[i][4];
- if (!activate_blocs[nextr][nextc] && !passed[nextr][nextc]) {
- cost ++;
- result = getDistance(nextr, nextc, dir_choices, activate_blocs, passed, cost);
- if (result != -1) {
- result[0] = nextr;
- result[1] = nextc;
- return result;
- }
- }
- }
- return -1;
- };
在羊駝移動(dòng)函數(shù)movePlayer中,首先通過getDistance來判斷上下左右4個(gè)方向,來尋找***移動(dòng)方向。根據(jù)getDistance的返回結(jié)果做相應(yīng)的邏輯處理。
游戲結(jié)束界面
游戲結(jié)束的兩種情況,玩家勝利或失敗。
在ResultUI的構(gòu)造函數(shù)中,加入?yún)?shù)win,用來標(biāo)識(shí)是否勝利。而勝利和失敗僅僅是顯示文字的區(qū)別,下方的兩個(gè)按鈕均一樣。
在ctor中,根據(jù)是否勝利加載不同的圖片來顯示:
- ctor : function (win) {
- this._super();
- this.win = win;
- if (win) {
- this.winPanel = new cc.Sprite(res.succeed);
- this.winPanel.x = cc.winSize.width/2;
- this.winPanel.anchorY = 0.2;
- this.winPanel.y = cc.winSize.height/2;
- this.addChild(this.winPanel);
- }
- else {
- this.losePanel = new cc.Sprite(res.failed);
- this.losePanel.x = cc.winSize.width/2;
- this.losePanel.anchorY = 0.2;
- this.losePanel.y = cc.winSize.height/2;
- this.addChild(this.losePanel);
- }
- }
在onEnter中,根據(jù)是否勝利加載不同的文字描述:
- if (this.win) {
- this.winPanel.removeAllChildren();
- var w = this.winPanel.width, h = this.winPanel.height;
- var label = new cc.LabelTTF("繼續(xù)刷屏!\n"+step+"步推倒我的小羊駝\n打敗"+percent+"%朋友圈的人!\n你能超過我嗎?", "宋體", 20);
- label.x = w/2;
- label.y = h/4;
- label.textAlign = cc.LabelTTF.TEXT_ALIGNMENT_CENTER;
- //label.boundingWidth = w;
- label.width = w;
- label.color = cc.color(0, 0, 0);
- this.winPanel.addChild(label);
- }
- else {
- this.losePanel.removeAllChildren();
- var w = this.losePanel.width, h = this.losePanel.height;
- label = new cc.LabelTTF("我滴小羊駝呀它又跑掉了\nT_T 快幫我抓回來!", "宋體", 20);
- label.x = w/2;
- label.y = h/4+5;
- label.textAlign = cc.LabelTTF.TEXT_ALIGNMENT_CENTER;
- //label.boundingWidth = w;
- label.width = w;
- label.color = cc.color(0, 0, 0);
- this.losePanel.addChild(label, 10);
- }
"通知好友"按鈕加載shareUI層,這個(gè)層其實(shí)是一個(gè)幫助指導(dǎo)界面,指示用戶點(diǎn)擊微信右上角的分享按鈕進(jìn)行分享。
- gameScene.addChild(layers.shareUI, 100);
- target.win ? share(1, step, percent) : share(2);
"再來一次"實(shí)現(xiàn)很簡(jiǎn)單,調(diào)用initGame重新初始化矩陣,并移除ResultUI層。
- layers.game.initGame();
- target.win ? layers.winUI.removeFromParent() : layers.loseUI.removeFromParent();
分享指導(dǎo)界面
在游戲結(jié)束界面我們加入了分享按鈕?,F(xiàn)在我們就來實(shí)現(xiàn)分享界面。
分享界面由分享圖標(biāo)和分享說明組成。這和前面的layer創(chuàng)建一樣。很簡(jiǎn)單,唯一的區(qū)別是,分享界面是cc.LayerColor(cc.LayerColor支持設(shè)置層的顏色)的子類。下面是實(shí)現(xiàn)代碼:
- ctor: function () {
- this._super(cc.color(0, 0, 0, 188), cc.winSize.width, cc.winSize.height);
- var arrow = new cc.Sprite(res.arrow);
- arrow.anchorX = 1;
- arrow.anchorY = 1;
- arrow.x = cc.winSize.width - 15;
- arrow.y = cc.winSize.height - 5;
- this.addChild(arrow);
- var label = new cc.LabelTTF("請(qǐng)點(diǎn)擊右上角的菜單按鈕\n再點(diǎn)\"分享到朋友圈\"\n讓好友們挑戰(zhàn)你的分?jǐn)?shù)!", "宋體", 20, cc.size(cc.winSize.width*0.7, 250), cc.TEXT_ALIGNMENT_CENTER);
- label.x = cc.winSize.width/2;
- label.y = cc.winSize.height - 100;
- label.anchorY = 1;
- this.addChild(label);
- },
加入touch事件用于移除分享界面:
- onEnter: function () {
- this._super();
- cc.eventManager.addListener({
- event: cc.EventListener.TOUCH_ONE_BY_ONE,
- onTouchBegan: function (touch, event) {
- layers.shareUI.removeFromParent();
- }
- }, this);
- }
微信分享
我們需要的功能:
1.分享到微信朋友圈
2.分享給微信好友
3.分享到騰訊微博
4.關(guān)注指定用戶
實(shí)現(xiàn)方式
本功能已經(jīng)有大神提供了完整的庫,地址是:https://github.com/zxlie/WeixinApi ,以下我們做一個(gè)簡(jiǎn)單的使用分析。
注:除特殊說明外,本小節(jié)實(shí)現(xiàn)均在文件 WeixinApi.js中。
現(xiàn)在我們實(shí)現(xiàn)的分享有,發(fā)給指定朋友,分享到朋友圈,分享到騰訊微博。對(duì)于不同的分享方式,實(shí)現(xiàn)方式大同小異,我們主要以分享到朋友圈為例。
分享數(shù)據(jù)
我們分享的時(shí)候需要的數(shù)據(jù)有:appid,圖片,鏈接,標(biāo)題,文字內(nèi)容,例如:
對(duì)應(yīng)在代碼中就需要以下數(shù)據(jù):
- "appid":theData.appId ? theData.appId : '',
- "img_url":theData.imgUrl,
- "link":theData.link,
- "desc":theData.desc,
- "title":theData.title, // 注意這里要分享出去的內(nèi)容是desc
數(shù)據(jù)來源
為了得到數(shù)據(jù),我們需要在GameScene.js中實(shí)現(xiàn)ResultUI的時(shí)候,將以上數(shù)據(jù)生成出來。 比如勝利時(shí),我們需要顯示:
- var label = new cc.LabelTTF("繼續(xù)刷屏!\n"+step+"步推倒我的小羊駝\n打敗"+percent+"%朋友圈的人!\n你能超過我嗎?", "宋體", 20);
完成數(shù)據(jù)后,我們需要判斷勝利或失敗,并傳回ui中顯示:
- target.win ? share(1, step, percent) : share(2);
分享回調(diào)
為了監(jiān)測(cè)分享的狀態(tài),無論分享成功與否我們回調(diào)都會(huì)上報(bào)狀態(tài),以便程序處理,我們需要的狀態(tài)有:
1.用戶取消分享
2.分享失敗
3.分享成功 所以我們需要以下實(shí)現(xiàn):
- WeixinJSBridge.on('menu:share:timeline', function (argv) {
- if (callbacks.async && callbacks.ready) {
- window["_wx_loadedCb_"] = callbacks.dataLoaded || new Function();
- if(window["_wx_loadedCb_"].toString().indexOf("_wx_loadedCb_") > 0) {
- window["_wx_loadedCb_"] = new Function();
- }
- callbacks.dataLoaded = function (newData) {
- window["_wx_loadedCb_"](newData);
- shareTimeline(newData);
- };
- // 然后就緒
- callbacks.ready && callbacks.ready(argv);
- } else {
- // 就緒狀態(tài)
- callbacks.ready && callbacks.ready(argv);
- shareTimeline(data);
- }
- });
WeixinJSBridge
在微信上,通過公眾平臺(tái)推送給用戶的文章,是在微信內(nèi)部直接打開的,用的無外乎就是一個(gè)UIWebView控件(IOS上,Android上也 差不多)。但特殊的是,微信官方在這里面加了一個(gè)默認(rèn)的Js API--WeixinJSBridge,通過它,能直接在該頁面上做分享操作。 以下代碼,拿去玩吧:
- WeixinJSBridge.on('menu:share:timeline', function (argv) {
- if (callbacks.async && callbacks.ready) {
- window["_wx_loadedCb_"] = callbacks.dataLoaded || new Function();
- if(window["_wx_loadedCb_"].toString().indexOf("_wx_loadedCb_") > 0) {
- window["_wx_loadedCb_"] = new Function();
- }
- callbacks.dataLoaded = function (newData) {
- window["_wx_loadedCb_"](newData);
- shareTimeline(newData);
- };
- // 然后就緒
- callbacks.ready && callbacks.ready(argv);
- } else {
- // 就緒狀態(tài)
- callbacks.ready && callbacks.ready(argv);
- shareTimeline(data);
- }
- });
***,趕緊寫點(diǎn)誘惑的東東,讓用戶分享出去吧,這是微信病毒傳播的樂趣!
后記
你可以在這里獲取本教程的全部 源碼 。