Cocos2d-JS實現的貪吃蛇

xuzhiguang 8年前發布 | 43K 次閱讀 游戲開發 Cocos2d-JS

一、前言

相信貪吃蛇大家都玩兒過,我對貪吃蛇的印象就是在電子詞典上,一只像素蛇在屏幕游走,饑渴難耐,看著豆子就要去吃,吃到豆子就會長一節,當蛇的身體越來越長的時候,它才發現這個世界變了,每走一步,都是寸步難行。<!--more-->當它的蛇頭觸碰到任意的物體,無論是屏幕邊界還是自己的身體,游戲都將結束。這款游戲應該是比較經典的一個童年記憶。剛接觸游戲開發的人可能比較喜歡以這款游戲入手,因為貪吃蛇包含了很多游戲開發中的原理,并且難度也不大,而我剛好在學習Cocos CVP的課程,學習在一個中間階段,我也來拿這個練練手,以下就把我做貪吃蛇的過程分享出來。

二、游戲分析

1.身體關節

貪吃蛇的實現可能有多種方法,但今天,我想用面向對象的思想來對游戲進行設計,到今天,任何的程序開發都離不開面向對象的思想,通過面向對象的思想我們能把很多抽象的問題具象化,方便我們解決很多問題。而在貪吃蛇中,面向對象的思想依然實用。
在貪吃蛇中,我們可以把一條游走的蛇的每個關節當做是一個對象,而蛇本身是由多個關節組成的整體,當每個關節在移動時,我們就能看到整個蛇的移動,每個關節的位置以及移動方向都跟它的上一個關節息息相關,那么我們就可以把關節與它的上一個關節關聯起來,實現如下結構:

Cocos2d-JS實現的貪吃蛇

蛇關節關系圖

2.移動方向

如上文所說,按照上面的關節關系來實現,那么蛇的移動方向就是與父節點的移動方向相關聯,每一個關節應該有一個當前移動方向和下次移動方向,每一步的移動,都是跟著當前移動方向走的,而父關節的當前移動的方向即為子關節的下次移動方向,這樣,只需要調整蛇頭關節的下次移動方向,整條蛇就能順著各自的父關節方向移動,蛇的移動方向圖如下:

Cocos2d-JS實現的貪吃蛇

蛇的移動方向圖

二、開發設計

1.項目結構

按照前面的設計,我們可以大致可以劃分出游戲場景類和關節類,進入游戲場景類,再將一些全局的變量單獨存在一個類中,項目結構可劃分如下圖:

Cocos2d-JS實現的貪吃蛇

項目結構圖

2.全局變量類

游戲中的全局變量,提煉為一個全局變量類,其中參數可以根據需求靈活變動配置

var Constants = {
    frequency: 0.2,// 刷新頻率
    speed: 31,// 每幀移動距離,身體節點大小+1像素間隔
    errDistance: 10,// 偏差舉例
}

3.關節類

按照以上設計的結構,每一個關節對象都應該包含蛇的當前方向、下次方向和蛇的父節點三個屬性,代碼如下:

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方法中,代碼如下:

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.頭部關節是否觸碰到自身關節

其中1、3條件達成,則判定游戲結束,2條件達成,則能增加游戲分數,并且游戲繼續。
move方法代碼如下:

// 關節移動方法
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;
}

4.游戲場景類

在游戲場景中我們需要以下幾個變量:

1. 貪吃蛇數組:用于存儲貪吃蛇所有的關節節點

  1. 貪吃蛇尾部:每添加一個關節節點 ,都將此變量指向這個新加的節點,以便下次繼續再尾部節點添加
  2. 吃的星星:屏幕中隨機產生的星星,用于判斷頭部關節是否與它產生碰撞
  3. 是否添加節點:如果在定時任務中判斷到吃到星星,那么可以次變量為1,代表可以添加一個節點
  4. 分數:存儲游戲中累加的分數</code></pre>

    在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完了之后,將下次移動方法變為本次移動方向
  5. 如果需要新增關節,在遍歷完成之后,新增一個關節類,并將其父節點指向之前的蛇尾節點,并把蛇尾指向新加的這個關節</code></pre>

    代碼如下:

    // 蛇關節移動方法
    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;
    
    } }</code></pre>

    目前為止這條蛇是只會按照我們初始化的方向一直走到碰壁,然后游戲結束的,如何改變蛇的運動軌跡呢?前面說到了,蛇頭部節點的下次移動方向的改變,即可對整個蛇的移動軌跡進行改變,這里我們可以通過點擊屏幕實現蛇頭的下次移動方向的改變。
    首先在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; }</code></pre>

    最后我們只差最后一步,就是蛇要吃的星星,我們可以在屏幕中任意位置隨機產生一顆星星(又或者叫豆子,這都無所謂),只要這個星星滿足以下條件,那么它就可以被繪制出來,否則我們需要重新隨機這個星星的位置:

    1. 星星在游戲場景的屏幕范圍內
  6. 星星不能與蛇的身體部分重疊</code></pre>

    代碼如下:

    // 更新星星
    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;
         }
     }
    
    } }</code></pre>

    至此,游戲的主要邏輯就大功告成了!貪吃蛇不僅能在屏幕中游走,還能吃星星,并且碰到自身或邊緣都會GameOver!

    5.開始/結束場景類

    說到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); } });</code></pre>

與結束場景一樣,開始場景也只需一個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); } });</code></pre>

這樣,我們就能形成一個完成游戲流程了,游戲加載進入游戲開始場景,點擊開始游戲進行如主游戲場景,游戲結束后進入結束場景,結束場景點擊“再來一次”又可以回到開始場景。

四、運行效果

最后的運行效果如下

Cocos2d-JS實現的貪吃蛇

貪吃蛇效果圖


通過CVP平臺的項目托管可看到實際運行效果,地址如下:
http://www.cocoscvp.com/usercode/2e17b3cd9586a574140e0bb765bad21673fc7686/

五、源代碼

所有源代碼均上傳到github,歡迎交流學習,地址:
https://github.com/hjcenry/snake

 

來自:Henry Blog

 

 

 本文由用戶 xuzhiguang 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!