進撃の贪食蛇 完整教程 by:zbycookie

第三节 蛇的建立

这一节我们开始创建关于蛇的类了,先创建一个Snake类,让他继承Node类,为什么不是继承Sprite类呢,要问这里的原因嘛,
请戳http://www.cnblogs.com/anndaming/archive/2012/05/20/2510285.html

Snake.h的代码如下

#ifndef __Snake__Snake__
#define __Snake__Snake__

#include 

#include "cocos2d.h"

using namespace cocos2d;

class Snake : public Node{
    
private:
    Sprite* snakenode;
public:
    void setNode(Sprite* node);
    Sprite* getNode();
    CREATE_FUNC(Snake);
    virtual bool init();

};


#endif /* defined(__Snake__Snake__) */


```


Snake.cpp的代码如下
#include "Snake.h"

Sprite* Snake::getNode(){
    return this->snakenode;
}

void Snake::setNode(Sprite* node){
    this->snakenode = node;
    this->addChild(snakenode);
}

bool Snake::init(){
    return true;
}

```

先实现Snake的create函数,用宏定义CREATE_FUNC();来实现。
好了,我们在Snake里定义一个Sprite类,然后实现他的get和set函数。
我们完成Snake的类之后定义一个SnakeHead类,为什么要定义SnakeHead类呢?,头和身体不一样啊,当然要分开定义了。让SnakeHead类先继承Snake类。

SnakeHead.h代码如下
#ifndef __Snake__SnakeHead__
#define __Snake__SnakeHead__

#include 

#include "cocos2d.h"
#include "Snake.h"

USING_NS_CC;

enum Direction {
    up = 0,
  down = 1,
  left = 2,
 right = 3,
};

class SnakeHead : public Snake{
private:
    Direction direction;
public:
    CREATE_FUNC(SnakeHead);
    virtual bool init();
    SnakeHead();
    ~SnakeHead();
    void setDirec(Direction di);
    Direction getDirec();
    
};

#endif /* defined(__Snake__SnakeHead__) */


```


SnakeHead.cpp代码如下
#include "SnakeHead.h"

bool SnakeHead::init(){
    return true;
}

SnakeHead::SnakeHead(){
    this->direction = up;
}

SnakeHead::~SnakeHead(){
    
}

Direction SnakeHead::getDirec(){
    return this->direction;
}

void SnakeHead::setDirec(Direction di){
    this->direction = di;
}


```

注意到了吧,我们在SnakeHead类里要添加头的方向,即Direction成员,然后实现他的set和get函数,这样的话,通过Head里的Direction成员我们才能知道蛇头应该往哪移动。
好了继续,我们往GameLayer.h里添加两个成员

```

SnakeHead* head;
Vector body;

```

第一个嘛,自然就是蛇头啦,第二个是蛇的身体,贪食蛇中蛇的身体是由很多节点构成的,所以我们用Vector来存储身体节点,Vector的具体用法后面会说

第三节结束

第四节 游戏界面的绘制

其实这一节和第一节差不多,主要就是在GameLayer类中添加一些按钮。
注意先include一下上一节的SnakeHead.h
打开GameLayer.h 往里面加入
成员:


    Sprite* background;
    MenuItemImage* butup;
    MenuItemImage* butdown;
    MenuItemImage* butleft;
    MenuItemImage* butright;
    MenuItemImage* pausebutton;
    MenuItemImage* playbutton;
    Label* gameover;
    Size visiblesize;
    Menu* menu;

简单解释一下,第一个成员是游戏界面的背景,第二到第四个成员是控制上下左右方向的按钮,第五个和第六个成员是控制游戏暂停和游戏恢复的按钮,第七个成员是用来显示“GameOver!”这句话的,第八个成员是屏幕可视大小,最后一个成员嘛,你懂的。

继续往GameLayer.h里添加成员函数


void SetApperance()
void SetDirection(Ref* psender,Direction direc);
void PauseGame();
void StartGame();

在GameLayer.cpp中实现 SetApperance()函数。代码如下

void GameLayer::SetApperance(){
    visiblesize = Director::getInstance()->getVisibleSize();
    
    background = Sprite::create("BackGround.png");
    background->setPosition(Point(visiblesize.width/2,visiblesize.height/2));
    this->addChild(background,0);  //this is the background
    
    butup = MenuItemImage::create("Button.png","Button2.png","Button.png",CC_CALLBACK_1(GameLayer::SetDirection,this,up));
    Size bs = butup->getContentSize();
    butup->setPosition(Point(visiblesize.width-3*bs.width+bs.width/2,3*bs.height+bs.height/2));

    butdown = MenuItemImage::create("Button.png","Button2.png","Button.png",CC_CALLBACK_1(GameLayer::SetDirection,this,down));
    butdown ->setPosition(Point(visiblesize.width-3*bs.width+bs.width/2,bs.height+bs.height/2));
    
    butleft = MenuItemImage::create("Button.png","Button2.png","Button.png",CC_CALLBACK_1(GameLayer::SetDirection,this,left));
    butleft ->setPosition(Point(visiblesize.width-4*bs.width+bs.width/2,2*bs.height+bs.height/2));
    
    butright = MenuItemImage::create("Button.png","Button2.png","Button.png",CC_CALLBACK_1(GameLayer::SetDirection,this,right));
    butright ->setPosition(Point(visiblesize.width-2*bs.width+bs.width/2,2*bs.height+bs.height/2));
    
    pausebutton = MenuItemImage::create("PauseButton.png","PauseButton.png",CC_CALLBACK_0(GameLayer::PauseGame,this));
    pausebutton->setPosition(Point(visiblesize.width - pausebutton->getContentSize().width,visiblesize.height - pausebutton->getContentSize().height/2));
    
    playbutton = MenuItemImage::create("rePlayButton1.png","rePlayButton2.png",CC_CALLBACK_0(GameLayer::StartGame, this));
    playbutton->setPosition(Point(visiblesize.width/2,visiblesize.height/2));
    playbutton->setVisible(false);
    
    menu = Menu::create(butup,butdown,butleft,butright,pausebutton,playbutton,NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu,2);
    
    TTFConfig ttfconfig("Marker Felt.ttf",40);
    gameover = Label::createWithTTF(ttfconfig, " Game Over !",TextHAlignment::CENTER);
    gameover->setPosition(Point(visiblesize.width/2,visiblesize.height/2+gameover->getContentSize().height));
    gameover->setVisible(false);
    gameover->setColor(Color3B::BLACK);
    this->addChild(gameover,2);
}

```


背景和按钮的设置我就不再提了,第一节里面都讲过了,稍微提一下就是第一节里的CC_CALLBACK_0 变成了CC_CALLBACK_1,因为我们要往回调函数SetDirection里面传一个Direction类型的参数。SetDirection函数先放一放,我们先来看如何创建一个label,首先我们要初始化一个ttfconfig结构体,结构体中有许多的成员,我们暂时只初始化前2个成员,分别是ttf文件的位置和字体的大小。配置完ttfconfig结构体后,我们开始创建label,利用createWithTTF函数来创建,前3个参数分别是ttfconfig结构体,你要输入的字符串,以及文字的对其方式,我们这里设置为居中。然后我们设置label的位置和颜色。

注意,这里我们先要将这个label设置为不可见,你可不想玩游戏的时候屏幕上一直有个 GameOver!吧,同样,恢复游戏的按钮也要设置为不可见。

最后用addChild函数添加成员。

好了现在我们已经完成了SetApperance函数了,在init函数中添加SetApperance函数吧!
接下来,我们开始实现SetDirection这个函数,注意如果用CC_CALLBACK1往回调函数里传参数的话,在定义回调函数的时候,要在函数参数中加入Ref* 的一个成员
代码如下
void GameLayer::SetDirection(Ref* psender,Direction direc){
    Direction headdir = head->getDirec();
        switch (direc) {
            case up:
                if(headdir==left||headdir==right){
                    head->setDirec(up);
                }
                break;
            case down:
                if(headdir==left||headdir==right){
                    head->setDirec(down);
                }
                break;
            case left:
                if(headdir==up||headdir==down){
                    head->setDirec(left);
                }
                break;
            case right:
                if(headdir==up||headdir==down){
                    head->setDirec(right);
                }
                break;
            default:
                break;
        }   
}


```

在这个函数中我们先获取蛇头的方向,然后根据蛇头的方向判断是否可以更改方向,因为当蛇在上下运动的时候,想改变方向的话只能变成左右,同理当蛇在左右运动的时候,只能改成上下运动。简而言之就是不能改成与当前蛇运动相反的方向。

第四节结束

第五节 初始化蛇

在GameLayer.h中添加 void SetSnake()函数
代码如下

void GameLayer::SetSnake(){
    body.clear();
    Size visiblesize = Director::getInstance()->getVisibleSize();
    head = SnakeHead::create();
    head->setNode(Sprite::create("SnakeHead.png"));
    head->setPosition(Point(visiblesize.width/2-head->getNode()->getContentSize().width/2,visiblesize.height/2-head->getNode()->getContentSize().height/2));
    this->addChild(head,1);
    
    for (int i=1 ; i < 4; i++) {
        Snake* bodynode = Snake::create();
        bodynode->setNode(Sprite::create("Snake.png"));
        bodynode->setPosition(Point(head->getPosition().x+i*bodynode->getNode()->getContentSize().width,head->getPosition().y));
        body.pushBack(bodynode);
        this->addChild(bodynode);
    }
    
}

```

我们先初始化蛇头,然后设置蛇头的位置,注意到addchid函数中后面的参数了吧,假设其为z,z指该成员的层级(也可以说深度),z值大的成员在z值小的成员的上面;默认的z是0。除了z,addchild还有一个tag参数,这个参数可以让你可以方便的找到需要的成员,不过我们这里用不到。

好了初始化完蛇头之后,我们开始初始化蛇的身体,一开始,我们定义3个蛇的身体节点,和蛇头一样,先初始化蛇的身体节点,然后设置节点位置,最后将身体节点加入body中,并添加到layer里,这样整条蛇的初始化就结束了,将SetSnake函数添加到init函数中去吧。

本节结束(这节好短);
放一张效果图吧。
 

好帖,浅显易懂,给的链接也有很大的阅读价值。

第六节 实现蛇的运动

我们往GameLayer.h中添加
成员函数:


void MoveStep();
void MoveBody();
virtua void update(float dt);

我们先写MoveBody这个函数。
MoveBody这个函数的作用是移动蛇所有的身体节点。
他的原理就是将每个身体节点的位置设为前一个身体节点的位置,其中如果是第一个身体节点的话,就用蛇头的位置来设置身体节点的位置。
代码如下

void GameLayer::MoveBody(){
    int n=body.size();
    Point HeadPosition = head->getPosition();
    Point a,temp;
    for (int i = 0; i < n; i++) {
        if(i==0){
            a = body.at(i)->getPosition();
            body.at(i)->setPosition(HeadPosition);
        }
        else {
            temp = a;
            a = body.at(i)->getPosition();
            body.at(i)->setPosition(temp);
         }
    }
    
}

```


接着,实现MoveStep函数
代码如下
void GameLayer::MoveStep(){
    movedflag = true;
    Direction temp = head->getDirec();
    Point po = head->Node::getPosition();
    switch (temp) {
        case up:
            //log("upupup");
            po.y += 20;
            break;
        case down:
            po.y -= 20;
            break;
        case left:
            po.x -= 20;
            break;
        case right:
            po.x += 20;
            break;
            
        default:
            break;
    }
    MoveBody();
    head->setPosition(po);
    
}

```


函数中,我们先获得蛇头运动方向,然后根据蛇头运动的方向来设置蛇头接下来的位置。

接着将MoveStep加入到update函数中,并在init函数中添加下面这行代码

```
this->schedule(schedule_selector(GameLayer::update), 0.6);
```


好了,我们现在已经实现蛇的运动啦。

第六节结束

第七节 放置食物及吃掉食物的判定

实现蛇的运动后我们就开始放置食物了,首先在GameLayer.h中添加
成员


Sprite* food;
Point lastbodyposi;

函数


void SetFood();
Point RandomPosition()
bool ifCollideBody(Point pos);
bool ifGetFood();
void AddBody();

首先是SetFood函数
代码如下

void GameLayer::SetFood(){
    this->removeChild(food);
    Point foodposi = RandomPosition();
    Point headposi = head->getPosition();
    while (foodposi==headposi||ifCollideBody(foodposi)) {
        foodposi = RandomPosition();
    }
    food = Sprite::create("Food.png");
    food->setPosition(foodposi);
    this->addChild(food);
}


```

里面有用到RandomPosition和ifCollideBody两个函数,
先说RandomPosition,代码如下
Point GameLayer::RandomPosition(){
    int x = (arc4random()%24);
    int y = (arc4random()%16);
    Point position = Point(x*20+10,y*20+10);
    return position;
}


```

这个函数返回的是一个Point类型的随机点

再是ifCollideBody函数,代码如下
bool GameLayer::ifCollideBody(Point pos){
    bool value = false;
    Snake* node;
    for (int i =0; igetPosition();
        if(nodepos==pos){
            value = true;
        }
    }
    return value;
}


```

这个函数的作用主要是检查给定的位置是否与蛇的身体节点重合,重合返回true,不重合返回false。

回到SetFood这个函数,因为整个游戏画面上同时只能存在一个食物,因此,当每执行SetFood的时候,要把前面设置的食物从layer中移除,所以要执行一下removeChildh函数。
接下来先获取蛇头的位置,然后产生一个随机点,检查该随机点是否与蛇头和蛇身体重合,如果重合,则继续生成随机点,直到生成的随机点与蛇头蛇身不重合为止。然后设置食物的位置为该随机点,设置食物的步骤就结束了。

接下来我们判断食物是否被吃掉,用到的是ifGetFood这个函数
代码如下
bool GameLayer::ifGetFood(){
    bool value = false;
    if(food->getPosition() == head->getPosition()){
        value = true;
    }
    return value;
}


```

非常简单吧
如果食物被吃掉,则调用AddBody函数代码如下
void GameLayer::AddBody(){
    head->setPosition(food->getPosition());
    Snake* node = Snake::create();
    node->setNode(Sprite::create("Snake.png"));
    node->setPosition(lastbodyposi);
    body.pushBack(node);
    this->addChild(node);
}


```

我们要在蛇的末尾加入一个身体节点,就要用到lastbodyposi这个成员了,这个成员保存的是上一步运动过程中,蛇的最后一个身体节点的位置。
这个lastbodyposi是在update函数中更新的,我们先来看一下update函数
void GameLayer::update(float dt){
        MoveStep();
        if(ifGetFood()){
            AddBody();
            SetFood();
        }
        lastbodyposi = body.at(body.size()-1)->getPosition();   
}

```


第七节结束

第八节 游戏结束的判定

贪食蛇里GameOver的原因只有2种,1是蛇头碰到身体,2是蛇头碰到墙壁

我们在GameLayer.h里添加
成员

 bool ifgameover;

函数


bool OutofRange();
bool HeadCollideBody(Direction headdirec);
void GameOver();
void PauseGame();
void SetSnakeVisible(bool value);

先说OutofRange函数,代码如下

bool GameLayer::OutofRange(){
    Point po = head->getPosition();
    if (po.x < 0||po.x > visiblesize.width||po.y < 0||po.y > visiblesize.height) {
        return true;
    }
    return  false;
}


```

这个函数用来判断蛇头是否超出游戏区域。

接下来是HeadCollideBody,代码如下
bool GameLayer::HeadCollideBody(Direction headdirec){
    float x = head->getPosition().x;
    float y = head->getPosition().y;
    bool iscollide = false;
    switch (headdirec) {
        case up:
            y += 20;
            break;
        case down:
            y -= 20;
            break;
        case left:
            x -= 20;
            break;
        case right:
            x += 20;
            break;
        default:
            break;
    }
    Point headnextpos = Point (x,y);
    iscollide = ifCollideBody(headnextpos);
    return  iscollide;
}

```

这个函数是用来判断蛇头下一步移动的位置是否与身体相撞。

再下来是GameOver函数,代码如下
void GameLayer::GameOver(){
    PauseGame();
    SetSnakeVisible(false);
    playbutton->setPosition(Point(visiblesize.width/2,visiblesize.height/2-gameover->getContentSize().height));
    gameover->setVisible(true);
    ifgameover = true;
}


```

里面又用到了另外两个函数,PauseGame和SetSnakeVisible。
PauseGame函数的作用主要是用来停止游戏,代码如下
void GameLayer::PauseGame(){
    
    this->pause();
    SetSnakeVisible(false);
    playbutton->setVisible(true);
    
}


```

SetSnakeVisible就是用来显示或隐藏蛇和食物的,代码如下
void GameLayer::SetSnakeVisible(bool val){
    food->setVisible(val);
    head->getNode()->setVisible(val);
        for (int i =0; isetVisible(val);
        }
}

```

回到GameOver函数,为了美观,我们需要调整一下开始按钮的位置,因为我们还要在开始按钮上面显示“GameOver!”的label,接着将ifgameover设置为true。
最后将update函数改为下面这个样子
void GameLayer::update(float dt){
    if (!HeadCollideBody(head->getDirec()) && !OutofRange()) {
         MoveStep();
        if(ifGetFood()){
            AddBody();
            SetFood();
        }
        lastbodyposi = body.at(body.size()-1)->getPosition();
    }
    else{
        GameOver();
    }
}


```


第八节结束

第九节 游戏的暂停和恢复

我们往GameLayer.h里添加
函数

void StartGame();

StartGame主要是用来恢复游戏的,是playbutton的回调函数,先看代码

void GameLayer::StartGame(){
    if(!ifgameover){
        SetSnakeVisible(true);
        this->resume();
    playbutton->setVisible(false);
    }else{
        this->unscheduleAllSelectors();
        this->removeAllChildren();
        body.clear();
        this->init();
        
    }
}

```


游戏过程中,在两种情况下我们会去点击开始游戏按钮
第一种是人为的暂停,恢复游戏时就要恢复游戏暂停前的状态
第二种是GameOver了,我们需要重新开始游戏。

这里我们用到了上一节的ifgameover来判断具体是哪种情况。
注意,在init函数中我们要将ifgameover设置为false。
bool GameLayer::init(){
    bool bRect = false;
    
    do {
        ifgameover = false;
        SetApperance();
        SetSnake();
        SetFood();
        this->schedule(schedule_selector(GameLayer::update), 0.6);
        bRect = true;
    } while (0);
    
    return bRect;
}



```


第九节 结束

最终章 一个小bug的解决

在玩的过程中发现了一个小bug,如果方向切换的过快,就会造成HeadCollideBody的误判
解决方法如下

在GameLayer.h中添加一个成员

bool movedflag;

然后修改 SetDirection函数

void GameLayer::SetDirection(Ref* psender,Direction direc){
    Direction headdir = head->getDirec();
    if (movedflag && direc!= headdir) {
        switch (direc) {
            case up:
                if(headdir==left||headdir==right){
                    head->setDirec(up);
                }
                break;
            case down:
                if(headdir==left||headdir==right){
                    head->setDirec(down);
                }
                break;
            case left:
                if(headdir==up||headdir==down){
                    head->setDirec(left);
                }
                break;
            case right:
                if(headdir==up||headdir==down){
                    head->setDirec(right);
                }
                break;
            default:
                break;
        }
        movedflag = false;
    }else{
    
    }    
}


```


然后在MoveStep函数的第一行添加一行代码

```
movedflag = true;
```


解决!

 

哇你这个好都告诉写在哪个文件了,对我这种菜鸟来说太好了,不过这是ios的么?适用于android吗

楼主,我只能点32个赞!

太棒了!这个修改一下直接就可以商业化了。:801::801::801:

适用的哦,就是导出app的时候方式不一样

还想了好几个点子呢,有一个就是蛇的节点是有磁铁组成的分 N级和S级,食物产生的时候也有N级和S级之分,具体怎么用这个还没想好
嘛,这只是个开始啦~

您的文章已被推荐到CocoaChina首页热门文章精选,感谢您的分享。:14:

大赞,学习,学习!!

真详细啊,吓得我都趴地上了

学习了。。

感谢!非常详细!非常适合新手!

楼主能发个项目压缩包给我吗,你这个文件我打不开。476855582@qq.com 多谢给赞

新手学习下