相信貪吃蛇你們都玩兒過,我對貪吃蛇的印象就是在電子詞典上,一隻像素蛇在屏幕遊走,飢渴難耐,看着豆子就要去吃,吃到豆子就會長一節,當蛇的身體愈來愈長的時候,它才發現這個世界變了,每走一步,都是步履維艱。 當它的蛇頭觸碰到任意的物體,不管是屏幕邊界仍是本身的身體,遊戲都將結束。這款遊戲應該是比較經典的一個童年記憶。剛接觸遊戲開發的人可能比較喜歡以這款遊戲入手,由於貪吃蛇包含了不少遊戲開發中的原理,而且難度也不大,而我恰好在學習Cocos CVP的課程,學習在一箇中間階段,我也來拿這個練練手,如下就把我作貪吃蛇的過程分享出來。javascript
貪吃蛇的實現可能有多種方法,但今天,我想用面向對象的思想來對遊戲進行設計,到今天,任何的程序開發都離不開面向對象的思想,經過面向對象的思想咱們能把不少抽象的問題具象化,方便咱們解決不少問題。而在貪吃蛇中,面向對象的思想依然實用。
在貪吃蛇中,咱們能夠把一條遊走的蛇的每一個關節當作是一個對象,而蛇自己是由多個關節組成的總體,當每一個關節在移動時,咱們就能看到整個蛇的移動,每一個關節的位置以及移動方向都跟它的上一個關節息息相關,那麼咱們就能夠把關節與它的上一個關節關聯起來,實現以下結構:
java
如上文所說,按照上面的關節關係來實現,那麼蛇的移動方向就是與父節點的移動方向相關聯,每個關節應該有一個當前移動方向和下次移動方向,每一步的移動,都是跟着當前移動方向走的,而父關節的當前移動的方向即爲子關節的下次移動方向,這樣,只須要調整蛇頭關節的下次移動方向,整條蛇就能順着各自的父關節方向移動,蛇的移動方向圖以下:
node
按照前面的設計,咱們能夠大體能夠劃分出遊戲場景類和關節類,進入遊戲場景類,再將一些全局的變量單獨存在一個類中,項目結構可劃分以下圖:
git
遊戲中的全局變量,提煉爲一個全局變量類,其中參數能夠根據需求靈活變更配置github
var Constants = { frequency: 0.2,// 刷新頻率 speed: 31,// 每幀移動距離,身體節點大小+1像素間隔 errDistance: 10,// 誤差舉例 }
按照以上設計的結構,每個關節對象都應該包含蛇的當前方向、下次方向和蛇的父節點三個屬性,代碼以下:數組
var SnakeBody = cc.Sprite.extend({ frontBody: null,//上一個身體關節,沒有則爲頭部 nextDirection: 0,// 1-上,2-下,3-左,4-右 direction: 0,// 1-上,2-下,3-左,4-右 ctor: function (frontBody, direction) { this._super(); this.frontBody = frontBody; this.direction = direction; this.nextDirection = direction; return true; } }
上面說的,關節的位置跟它的福關節的位置是息息相關的,那麼初始化的時候,咱們就須要根據父關節的移動方向來進行次關節的位置設置,這部分代碼,咱們能夠放在onEnter方法中,代碼以下:dom
onEnter: function () { this._super(); if (this.frontBody == null) { // 蛇頭部關節,設置頭部紋理 switch (this.direction) { case 1: this.setTexture(res.head_up); break; case 2: this.setTexture(res.head_down); break; case 3: this.setTexture(res.head_left); break; case 4: this.setTexture(res.head_right); break; } } else { // 蛇身體關節 // 設置紋理 this.setTexture(res.body); // 設置關節位置 var frontX = this.frontBody.getPositionX(); var frontY = this.frontBody.getPositionY(); var frontWidth = this.frontBody.width; var frontHeight = this.frontBody.height; var width = this.width; var height = this.height; switch (this.frontBody.direction) { // 根據父關節的當前移動方向,決定此關節的位置 case 1:// 上 this.setPosition(frontX, frontY - frontHeight / 2 - height / 2 - 1); break; case 2:// 下 this.setPosition(frontX, frontY + frontHeight / 2 + height / 2 + 1); break; case 3:// 左 this.setPosition(frontX + frontWidth / 2 + width / 2 + 1, frontY); break; case 4:// 右 this.setPosition(frontX - frontWidth / 2 - width / 2 - 1, frontY); break; } } return true; }
有了這三個屬性,每個節點還應該有最重要的遊戲邏輯——move方法,每個關節分別調用move方法,從遊戲場景中就能看到整條蛇按照預約方向進行移動,而整條蛇的運動方向就是跟着頭部關節的方向走,頭部關節的方向則經過點擊屏幕區域控制。
在move方法中,咱們須要作如下事情:函數
1.按照關節的下次移動方向移動自己長度的像素的距離 2.若是是頭部關節,須要改變關節紋理
同時,若是是頭部關節,咱們還須要判斷如下三個臨界條件:學習
1.頭部關節是否觸碰到屏幕邊界 2.頭部關節是否吃到屏幕中的豆子 3.頭部關節是否觸碰到自身關節
其中一、3條件達成,則斷定遊戲結束,2條件達成,則能增長遊戲分數,而且遊戲繼續。
move方法代碼以下:this
// 關節移動方法 move: function (layer) { var star = layer.star; var direct; if (this.frontBody == null) { // 頭部關節按照自身的下次方向行走 direct = this.nextDirection; } else { // 身體關節按照父關節的當前方向行走,並將福關節的當前方向設置爲自身的下次方向 this.nextDirection = direct = this.frontBody.direction; } switch (direct) { case 1:// 上 this.setPosition(this.getPositionX(), this.getPositionY() + Constants.speed); // this.runAction(cc.moveBy(Constants.frequency, cc.p(0, Constants.speed), 0)) break; case 2:// 下 this.setPosition(this.getPositionX(), this.getPositionY() - Constants.speed); // this.runAction(cc.moveBy(Constants.frequency, cc.p(0, -Constants.speed), 0)) break; case 3:// 左 this.setPosition(this.getPositionX() - Constants.speed, this.getPositionY()); // this.runAction(cc.moveBy(Constants.frequency, cc.p(-Constants.speed, 0), 0)) break; case 4:// 右 this.setPosition(this.getPositionX() + Constants.speed, this.getPositionY()); // this.runAction(cc.moveBy(Constants.frequency, cc.p(Constants.speed, 0), 0)) break; } if (this.frontBody == null) { switch (this.nextDirection) { // 頭部關節須要設置頭部不一樣方向的紋理 case 1:// 上 this.setTexture(res.head_up); break; case 2:// 下 this.setTexture(res.head_down); break; case 3:// 左 this.setTexture(res.head_left); break; case 4:// 右 this.setTexture(res.head_right); break; } // 頭部關節判斷是否觸碰到邊界 var size = cc.winSize; if ((this.getPositionX() > size.width - this.width / 2) || (this.getPositionX() < this.width / 2) || (this.getPositionY() > size.height - this.height / 2) || (this.getPositionY() < this.height / 2)) { // 判斷觸碰邊界 cc.log("game over"); return false; } // 判斷是否觸碰到本身身體關節 for (var index in layer.bodys) { if (layer.bodys[index] != this && cc.rectIntersectsRect(this.getBoundingBox(), layer.bodys[index].getBoundingBox())) { return false; } } // 判斷是否吃到星星 if (star != null) { if (cc.rectIntersectsRect(this.getBoundingBox(), star.getBoundingBox())) { star.runAction( cc.sequence(cc.spawn( cc.scaleTo(0.2, 3), cc.fadeOut(0.2) ), cc.callFunc(function (star) { star.removeFromParent(); }, star)) ); // 清除星星 layer.star = null; // 添加身體 layer.canNewBody = 1; // 改變分數 layer.score.setString("" + (Number(layer.score.getString()) + Math.round(Math.random() * 3 + 1))); layer.score.runAction(cc.sequence(cc.scaleTo(0.1, 2), cc.scaleTo(0.1, 0.5), cc.scaleTo(0.1, 1))); } } } return true; }
在遊戲場景中咱們須要如下幾個變量:
1. 貪吃蛇數組:用於存儲貪吃蛇全部的關節節點 2. 貪吃蛇尾部:每添加一個關節節點 ,都將此變量指向這個新加的節點,以便下次繼續再尾部節點添加 3. 吃的星星:屏幕中隨機產生的星星,用於判斷頭部關節是否與它產生碰撞 4. 是否添加節點:若是在定時任務中判斷到吃到星星,那麼能夠次變量爲1,表明能夠添加一個節點 5. 分數:存儲遊戲中累加的分數
在cc.Layer的構造函數中對以上變量進行初始化,代碼以下:
var GameLayer = cc.Layer.extend({ bodys: [],// snake body tail: null,// snake tail star: null,// star canNewBody: 0,// 0-無,1-有 score: null,// 分數Label ctor: function () { // 初始化全局參數 this._super(); this.bodys = []; this.canNewBody = 0; this.star = null; this.tail = null; this.score = null; return true; } }
以後,咱們首先須要在場景中繪製出一條蛇,初始化定義爲1個頭部關節,5個身體關節,因爲咱們對關節類作了很好的封裝,因此初始化一條蛇的代碼很簡單,咱們在onEnter方法中進行初始化,以下所示:
// 初始化一條蛇 // 初始化頭部 var head = new SnakeBody(null, 4); head.setPosition(300, 300); this.addChild(head); this.bodys.push(head); head.setTag(1); this.tail = head; // 循環添加5個身體 for (var i = 0; i < 5; i++) { var node = new SnakeBody(this.tail, this.tail.direction); this.addChild(node); this.bodys.push(node); this.tail = node; }
初始化完了以後蛇是不會動的,如何讓它動起來呢,咱們就要用到在關節類中封裝的move方法了,咱們每隔一個時間,對全部的關節類執行一次move方法,就能實現蛇的移動,首先在onEnter中添加定時任務:
// 蛇移動的定時任務 this.schedule(this.snakeMove, Constants.frequency);
在這個snakeMove定時調用的方法中,咱們要寫出全部關節移動的邏輯,在這個方法中,咱們須要完成如下幾件事:
1. 遍歷蛇的全部關節,每一個關節執行一遍move方法,並在move完了以後,將下次移動方法變爲本次移動方向 2. 若是須要新增關節,在遍歷完成以後,新增一個關節類,並將其父節點指向以前的蛇尾節點,並把蛇尾指向新加的這個關節
代碼以下:
// 蛇關節移動方法 snakeMove: function () { for (var index in this.bodys) { // 循環執行移動方法,並返回移動結果,false即視爲遊戲結束 if (!this.bodys[index].move(this)) { // 執行移動方法,移動失敗,遊戲結束 this.unschedule(this.snakeMove); this.unschedule(this.updateStar); var overScene = new OverScene(Number(this.score.getString()), false); cc.director.runScene(new cc.TransitionFade(1, overScene)); } } for (var index in this.bodys) { // 本輪全部關節移動結束,全部節點的當前方向賦值爲下一次的方向 this.bodys[index].direction = this.bodys[index].nextDirection; } if (this.canNewBody == 1) { // 若是新增關節爲1,增長關節 var node = new SnakeBody(this.tail, this.tail.direction); this.addChild(node); this.bodys.push(node); this.tail = node; this.canNewBody = 0; } }
目前爲止這條蛇是隻會按照咱們初始化的方向一直走到碰壁,而後遊戲結束的,如何改變蛇的運動軌跡呢?前面說到了,蛇頭部節點的下次移動方向的改變,便可對整個蛇的移動軌跡進行改變,這裏咱們能夠經過點擊屏幕實現蛇頭的下次移動方向的改變。
首先在onEnter方法中添加觸摸事件監聽:
// 添加屏幕觸摸事件 cc.eventManager.addListener({ event: cc.EventListener.TOUCH_ONE_BY_ONE, swallowTouches: true, onTouchBegan: this.touchbegan, onTouchMoved: this.touchmoved, onTouchEnded: this.touchended }, this);
而後在onTouchBegan方法中實現點擊事件,咱們能夠容許點擊有一個10像素的偏差:
// 點擊轉向 touchbegan: function (touch, event) { var x = touch.getLocation().x; var y = touch.getLocation().y; var head = event.getCurrentTarget().getChildByTag(1); var headX = head.getPositionX(); var headY = head.getPositionY(); switch (head.direction) { case 1:// 上 case 2:// 下 if (x <= headX - Constants.errDistance) {// 轉左 head.nextDirection = 3; } else if (x >= headX + Constants.errDistance) {// 轉右 head.nextDirection = 4; } break; case 3:// 左 case 4:// 右 if (y <= headY - Constants.errDistance) {// 轉下 head.nextDirection = 2; } else if (y >= headY + Constants.errDistance) {// 轉上 head.nextDirection = 1; } break; } return true; }
最後咱們只差最後一步,就是蛇要吃的星星,咱們能夠在屏幕中任意位置隨機產生一顆星星(又或者叫豆子,這都無所謂),只要這個星星知足如下條件,那麼它就能夠被繪製出來,不然咱們須要從新隨機這個星星的位置:
1. 星星在遊戲場景的屏幕範圍內 2. 星星不能與蛇的身體部分重疊
代碼以下:
// 更新星星 updateStar: function () { if (this.star == null) { this.star = new cc.Sprite(res.bean); var randomX = Math.random() * (cc.winSize.width - this.star.width) + this.star.width; var randomY = Math.random() * (cc.winSize.height - this.star.width) + this.star.height; this.star.setPosition(randomX, randomY); this.addChild(this.star); // 產生的星星只要在屏幕外,或與蛇的身體部分重疊,則本次任務不產生 if ((randomX > cc.winSize.width - this.star.width / 2) || (randomX < this.star.width / 2) || (randomY > cc.winSize.height - this.star.height / 2) || (randomY < this.star.height / 2)) { cc.log("update star:out of screen"); this.removeChild(this.star); this.star = null; return; } for (var index in this.bodys) { if (cc.rectIntersectsRect(this.bodys[index].getBoundingBox(), this.star.getBoundingBox())) { cc.log("update star:intersect with self"); this.removeChild(this.star); this.star = null; return; } } } }
至此,遊戲的主要邏輯就大功告成了!貪吃蛇不只能在屏幕中游走,還能吃星星,而且碰到自身或邊緣都會GameOver!
說到GameOver,那麼就必需要有一個Over的場景類了,畢竟有了開始場景,遊戲場景和結束場景,纔算得上一個完成的遊戲流程嘛,結束場景類的實現很簡單,只須要把遊戲場景中得到的分數傳遞進來,而後在Label中展現便可,代碼以下:
var OverLayer = cc.Layer.extend({ sprite: null, score: 0, ctor: function (score) { this._super(); this.score = score; return true; }, onEnter: function () { this._super(); var size = cc.winSize; var over = new cc.LabelTTF("Game Over,你的分數是:" + this.score, "Arial", 38); over.setPosition(size.width / 2, size.height / 2); this.addChild(over); over.runAction(cc.sequence(cc.scaleTo(0.2, 2), cc.scaleTo(0.2, 0.5), cc.scaleTo(0.2, 1))); var start = new cc.MenuItemFont("再來一次", function () { cc.director.runScene(new cc.TransitionFade(1, new HelloWorldScene())); }, this); start.setPosition(over.getPositionX(), over.getPositionY() - over.height / 2 - 50); var menu = new cc.Menu(start); this.addChild(menu); menu.setPosition(0, 0); return true; } }); var OverScene = cc.Scene.extend({ score: 0, ctor: function (score) { this._super(); this.score = score; return true; }, onEnter: function () { this._super(); var layer = new OverLayer(this.score); this.addChild(layer); } });
與結束場景同樣,開始場景也只需一個Label一個Menu便可,代碼以下:
var HelloWorldLayer = cc.Layer.extend({ sprite: null, ctor: function () { this._super(); var size = cc.winSize; var helloLabel = new cc.LabelTTF("貪吃蛇", "Arial", 38); helloLabel.x = size.width / 2; helloLabel.y = size.height / 2 + 200; this.addChild(helloLabel, 5); var start = new cc.MenuItemFont("開始遊戲", function () { cc.director.runScene(new cc.TransitionFade(1, new GameScene())); }, this); 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); } });
這樣,咱們就能造成一個完成遊戲流程了,遊戲加載進入遊戲開始場景,點擊開始遊戲進行如主遊戲場景,遊戲結束後進入結束場景,結束場景點擊「再來一次」又能夠回到開始場景。
最後的運行效果以下
經過CVP平臺的項目託管可看到實際運行效果,地址以下:
http://www.cocoscvp.com/usercode/2e17b3cd9586a574140e0bb765bad21673fc7686/
全部源代碼均上傳到github,歡迎交流學習,地址:
https://github.com/hjcenry/snake
本人的簡書博客與我的博客將同步更新
原文來自我的博客http://hjcenry.github.io/2016/05/16/Cocos2d-JS實現的貪吃蛇/
我的博客地址:http://hjcenry.github.io/