简单粗暴的实现贪吃蛇

在开发准备前纠结自己算法不行会写出特别糟糕的代码,不过既然是技术交流还是决定按自己的想法写代码管那么多干嘛。代码撸得比较急,每天一小时左右搞了3天…
临时想的逻辑,可能存在bug,请不要吝啬指出。。。

环境:
cocos2d-x-3.0rc1
VS2012

首先看下自定义的生成蛇头蛇身体的精灵类:

class SnakeSprite : public cocos2d::Sprite
{
public:
    virtual bool init();
    CREATE_FUNC(SnakeSprite);

    // 头部的图片不一样  所以有vip特权加个特别的函数
    static SnakeSprite* create(const char*);
    
    // 设置后面一个节点的坐标
    void setNextPoint(cocos2d::Point); // 
    cocos2d::Point getNextPoint(); // 供后面的节点获取位置
protected:
    cocos2d::Point nextPoint;
};


```


这个就很简单的了,就是继承Sprite实现蛇身体的节点了,为什么继承Sprite?重点在于nextPoint这个属性上了,就是靠它维护蛇移动的。


接下来就是游戏场景,游戏场景设计是960,640的,蛇移动区域是760 , 560,蛇的宽高是40.之前在设计的时候就考虑界面上四个按钮控制方向,后面发现不怎么好操作就加了个划屏控制方向。发现也不怎么好控制,好吧  纠结了。
代码:

enum DIRECTION  // 四个方向tag和重新开始 菜单tag
{
    RIGHT = 0 , 
    LEFT ,
    DOWN , 
    UP , 
    RESTART , 
    MENU
};

#define UP_TIME .25f  // 蛇身体移动的时间间隔

class GameScene : public cocos2d::Layer
{

public:
    static cocos2d::Scene* createScene();
    virtual bool init();
    CREATE_FUNC(GameScene);

protected:
    virtual void onEnter();
    virtual void onExit();
    virtual bool onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *unused_event); // 监听屏幕触摸事件
    virtual void onTouchEnded(cocos2d::Touch *pTouch, cocos2d::Event *pEvent);
private:
    cocos2d::Layer* layerGameMap; // 蛇的活动范围
    cocos2d::Sprite* spriteFood; // 食物
    cocos2d::Sprite* spriteHeader; // 蛇的头部 
    cocos2d::Sprite* spriteFooter; // 蛇的默认节点  当作尾巴把
    cocos2d::MenuItem* menuItemRestart; // 重新开始按钮
    cocos2d::MenuItem* menuItemMenu; // 菜单按钮
    cocos2d::Label* labelScore; // 分数label
    cocos2d::Label* labelTime; // 时间label
    int gameScore; // 当前分数
    long gameTime; // 此局开始了多久

    // 因为更新时间是固定的  怕当前更新蛇坐标还没完成又开始更新可能导致问题 所以加个标识符
    // (看代码更容易理解)
    bool isMoveEnd;

    // 各种按钮CB函数
    void menuCloseCallback(cocos2d::Ref* pSender);

    // 默认头部和脚步  所以初始化会为2 用于记录蛇有多少节点
    int bones;

    // 蛇前进的方向
    DIRECTION currDirection;
    void changeDirection(DIRECTION);

    // 开始结束
    void start();
    void end();

    // 一定时间的回调,用于移动蛇节点和检查游戏状态
    void gameTask(float);
    void timeTask(float);

    // 移动
    void move();

    //检查是否自己咬自己 或者捡到食物
    void check();

    // 就是改变食物坐标 没必要重复创建就复用了
    void createFood();

    // 添加蛇身体节点
    void addBody();
};


```


#include "GameScene.h"
#include "SnakeSprite.h"
#include "SimpleAudioEngine.h"
#include "MenuScene.h"

USING_NS_CC;
using namespace CocosDenshion;

Scene* GameScene::createScene() {
    auto scene = Scene::create();
    auto layer = GameScene::create();
    scene->addChild(layer);
    return scene;
}


bool GameScene::init() {
    if(!Layer::init()){
        return false;
    }
    // 随机数因子
    srand((unsigned)time(NULL)); 
    SpriteFrameCache::getInstance()->addSpriteFramesWithFile("image.plist");

    auto visibleSize = Director::getInstance()->getVisibleSize();

    auto layerMain = Layer::create();
    layerMain->setContentSize(Size(960 , 640));
    layerMain->ignoreAnchorPointForPosition(false);
    layerMain->setPosition( visibleSize.width / 2 , visibleSize.height / 2 );
    this->addChild(layerMain);

    // 游戏背景图(算什么背景图 不都是黑色么。)
    auto spriteBackground = Sprite::createWithSpriteFrameName("background.png");
    spriteBackground->setPosition( 480 , 320 );
    layerMain->addChild(spriteBackground);

    // 游戏地图
    layerGameMap = Layer::create();
    layerGameMap->setContentSize(Size( 760 , 560 ));
    layerGameMap->setAnchorPoint(Point(0 ,0));
    layerGameMap->setPosition( 100 , 19.5f );
    spriteBackground->addChild(layerGameMap);

    // 添加食物
    spriteFood = Sprite::createWithSpriteFrameName("food.png");
    spriteFood->setAnchorPoint(Point(0,0));
    layerGameMap->addChild(spriteFood);

    // 创建默认蛇头部
    spriteHeader = SnakeSprite::create("snake-head-up.png");
    spriteHeader->setTag(0);
    spriteHeader->setPosition( 40 , 0 );
    layerGameMap->addChild(spriteHeader , 2);

    // 给蛇添加个尾巴
    spriteFooter = SnakeSprite::create();
    spriteFooter->setPosition( 0 , 0 );
    spriteFooter->setTag(1);
    layerGameMap->addChild(spriteFooter);

    // 时间分数
    labelScore = Label::create("Score: 0" , "" , 40);
    labelScore->setAnchorPoint(Point( 0 , .5f ));
    labelScore->setPosition(Point( 100 , 610 ));
    layerMain->addChild(labelScore);

    labelTime = Label::create("Time: 00:00:00" , "" , 35);
    labelTime->setAnchorPoint(Point( 0 , .5f ));
    labelTime->setPosition(Point( 620 , 610 ));
    layerMain->addChild(labelTime);

     // 各种菜单item初始化 代码恶心  请勿细看
    auto menuItemUp = MenuItemSprite::create(Sprite::createWithSpriteFrameName("up-normal.png") , 
        Sprite::createWithSpriteFrameName("up-press.png") , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemUp->setTag(UP);
    menuItemUp->setPosition( 50 , 80 );

    auto menuItemDown = MenuItemSprite::create(Sprite::createWithSpriteFrameName("down-normal.png") , 
        Sprite::createWithSpriteFrameName("down-press.png") , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemDown->setTag(DOWN);
    menuItemDown->setPosition( visibleSize.width - 50 , 80 );

    auto menuItemRight = MenuItemSprite::create(Sprite::createWithSpriteFrameName("right-normal.png") , 
        Sprite::createWithSpriteFrameName("right-press.png") , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemRight->setTag(RIGHT);
    menuItemRight->setPosition( visibleSize.width - 50 , 300 );

    auto menuItemLeft = MenuItemSprite::create(Sprite::createWithSpriteFrameName("left-normal.png") , 
        Sprite::createWithSpriteFrameName("left-press.png") , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemLeft->setTag(LEFT);
    menuItemLeft->setPosition( 50 , 300 );

    menuItemRestart = MenuItemFont::create("Restart" , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemRestart->setTag(RESTART);
    menuItemRestart->setVisible(false);
    menuItemRestart->setPosition( visibleSize.width / 2 , visibleSize.height / 2 );

    menuItemMenu = MenuItemFont::create("ReturnMenu" , CC_CALLBACK_1(GameScene::menuCloseCallback,this));
    menuItemMenu->setTag(MENU);
    menuItemMenu->setVisible(false);
    menuItemMenu->setPosition( visibleSize.width / 2 , visibleSize.height / 2 - 50 );

    auto menuMain = Menu::create(menuItemUp , menuItemDown , menuItemRight , menuItemLeft ,menuItemRestart ,menuItemMenu , NULL);
    menuMain->setPosition(Point::ZERO);
    this->addChild(menuMain);

    start();

    return true;
}

void GameScene::menuCloseCallback(cocos2d::Ref* pSender) {
    MenuItem* item = (MenuItem*)pSender;
    DIRECTION direction = (DIRECTION)item->getTag();
    switch (direction) {
    case UP:
    case DOWN:
    case RIGHT:
    case LEFT:
        changeDirection(direction);
        break;
    case RESTART:
        start();
        break;
    case MENU:
        {
            auto scene = MenuScene::createScene();
            Director::getInstance()->replaceScene(scene);
        }
        break;
    default:
        break;
    }
}

void GameScene::start() {
    menuItemRestart->setVisible(false);
    menuItemMenu->setVisible(false);
    for (int i = 2 ; i < bones ; i++) {
        layerGameMap->removeChildByTag(i);
    }
    gameScore = 0; // 分数
    bones = 2; //  头  尾
    gameTime = 0; // 时间
    labelTime->setString("Time: 00:00:00");
    labelScore->setString("Score: 0");
    isMoveEnd = false;
    currDirection = RIGHT;
    this->schedule(schedule_selector(GameScene::gameTask), UP_TIME);
    this->schedule(schedule_selector(GameScene::timeTask), 1.0f);
    createFood();
    spriteHeader->setSpriteFrame("snake-head-right.png");
    spriteHeader->setPosition( 40 , 0 );
    spriteFooter->setPosition( 0 , 0 );
}

void GameScene::gameTask(float t) {
    if(isMoveEnd){ return; };
    check();
    move();
}

void GameScene::end() {
    this->unschedule(schedule_selector(GameScene::gameTask));
    this->unschedule(schedule_selector(GameScene::timeTask));
    menuItemRestart->setVisible(true);
    menuItemMenu->setVisible(true);
    auto userData = UserDefault::getInstance();
    int lastScore = userData->getIntegerForKey("score" , 0);
    if(lastScore < gameScore){
        userData->setIntegerForKey("score" , gameScore);
        userData->flush();
    }
}

void GameScene::move() {
    isMoveEnd = true;
    for (int i = 0 ; i < bones ; i++) {
        auto snake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        auto snakePoint = snake->getPosition();
        SnakeSprite* preSnake = NULL;
        Point preSnakePoint;
        if(i == 0) {
            Point snakeCurrPoint;
            switch (currDirection) {
            case RIGHT:
                snakeCurrPoint = snakePoint + CCPoint(40 , 0);
                break;
            case LEFT:
                snakeCurrPoint = snakePoint + CCPoint(-40 , 0);
                break;
            case DOWN:
                snakeCurrPoint = snakePoint + CCPoint(0 , -40);
                break;
            case UP:
                snakeCurrPoint = snakePoint + CCPoint(0 , 40);
                break;
            default:
                break;
            }

            if(snakeCurrPoint.x >= 760 || snakeCurrPoint.x < 0 || snakeCurrPoint.y >= 560 || snakeCurrPoint.y < 0) {
                end();
                return;
            }
            snake->setPosition(snakeCurrPoint);
            snake->setNextPoint(snakePoint);
        }  else  {
            preSnake = (SnakeSprite*)layerGameMap->getChildByTag(i - 1);
            preSnakePoint = preSnake->getNextPoint();

            snake->setPosition(preSnakePoint);
            snake->setNextPoint(snakePoint);
        }
    }
    isMoveEnd = false;
}

void GameScene::check() {
    // 如果头部跟实物坐标一致则代表吃了一个食物
    if(spriteFood->getPosition() == spriteHeader->getPosition()) {
        SimpleAudioEngine::getInstance()->playEffect("eat.wav");
        addBody();
        createFood();
        gameScore += 5;
        labelScore->setString(CCString::createWithFormat("Score: %d" , gameScore)->getCString());
    }

    // 检查当前头部是否与身体节点重叠  重叠为自己咬自己  游戏结束
    for (int i = 1 ; i < bones ; i++) {
        auto spriteSnake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        if(spriteHeader->getPosition() == spriteSnake->getPosition()) {
            end();
        }
    }
}

void GameScene::addBody() {
    auto spriteBody = SnakeSprite::create();
    spriteBody->setTag(bones);
    layerGameMap->addChild(spriteBody);
    bones++;
}

void GameScene::createFood() {
    // 760 / 40       560 / 40
    int xNumder = (19 * CCRANDOM_0_1());
    int yNumder = (14 * CCRANDOM_0_1());
    Point pointFood = Point(xNumder * 40 , yNumder * 40);
    // 防止食物生成在蛇身体节点上
    for (int i = 0 ; i < bones ; i++) {
        auto spriteSnake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        if(spriteSnake->getPosition() == pointFood) {
            createFood();
            return;
        }
    }
    spriteFood->setPosition( xNumder * 40 , yNumder * 40);
}

void GameScene::timeTask(float) {
    gameTime++;
    int shi,fen,miao;
    shi = gameTime / 3600;
    fen = gameTime / 60 % 60;
    miao = gameTime % 60;
    labelTime->setString(CCString::createWithFormat("Time: %02d:%02d:%02d" , shi , fen , miao)->getCString());
}

void GameScene::onEnter() {
    Layer::onEnter();
    SimpleAudioEngine::getInstance()->preloadEffect("eat.wav");

    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan=CC_CALLBACK_2(GameScene::onTouchBegan , this);
    listener->onTouchEnded=CC_CALLBACK_2(GameScene::onTouchEnded,this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(listener,this);
}

void GameScene::onExit() {
    Layer::onExit();
    SimpleAudioEngine::getInstance()->unloadEffect("eat.wav");
}

bool GameScene::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *unused_event) {
    if(layerGameMap->getBoundingBox().containsPoint(touch->getLocation())){
        return true;
    }
    return false;
}

void GameScene::onTouchEnded(cocos2d::Touch *pTouch, cocos2d::Event *unused_event) {
    Point pointStart = pTouch->getStartLocation();
    Point pointEnd = pTouch->getLocation();
    int x = pointStart.x - pointEnd.x;
    int y = pointStart.y - pointEnd.y;
    if(abs(x) > abs(y))  { 
        if(x + 100 > 0)  { 
            changeDirection(LEFT);
        }  else  { 
            changeDirection(RIGHT);
        }
    }  else {
        if(y + 100 > 0)  { 
            changeDirection(DOWN);
        }  else  { 
            changeDirection(UP);
        }
    }
}

void GameScene::changeDirection(DIRECTION direction) {
    switch (direction) {
    case UP:
        if( currDirection == UP || currDirection == DOWN ){ return; }
        spriteHeader->setSpriteFrame("snake-head-up.png");
        break;
    case DOWN:
        if( currDirection == UP || currDirection == DOWN ){ return; }
        spriteHeader->setSpriteFrame("snake-head-down.png");
        break;
    case RIGHT:
        if( currDirection == RIGHT || currDirection == LEFT ){ return; }
        spriteHeader->setSpriteFrame("snake-head-right.png");
        break;
    case LEFT:
        if( currDirection == RIGHT || currDirection == LEFT ){ return; }
        spriteHeader->setSpriteFrame("snake-head-left.png");
        break;
    }
    currDirection = direction;
}


```


移动蛇身体的逻辑很简单,每个节点维护着下个节点的坐标,就是每次节点移动都会把当前坐标赋值给nextPoint 属性,然后下面的节点从上面的节点获取下个点的坐标nextPoint ,这样只需要控制头的方向就能控制整个身体了。说这么多自己都看不懂的废话。。。还是看代码把。

void GameScene::move() {
    isMoveEnd = true;
    for (int i = 0 ; i < bones ; i++) {
        auto snake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        auto snakePoint = snake->getPosition();
        SnakeSprite* preSnake = NULL;
        Point preSnakePoint;
        if(i == 0) { // 0是头,我们只需要控制头的方向和坐标
            Point snakeCurrPoint;
            switch (currDirection) {
            case RIGHT:
                snakeCurrPoint = snakePoint + CCPoint(40 , 0);
                break;
            case LEFT:
                snakeCurrPoint = snakePoint + CCPoint(-40 , 0);
                break;
            case DOWN:
                snakeCurrPoint = snakePoint + CCPoint(0 , -40);
                break;
            case UP:
                snakeCurrPoint = snakePoint + CCPoint(0 , 40);
                break;
            default:
                break;
            }

            if(snakeCurrPoint.x >= 760 || snakeCurrPoint.x < 0 || snakeCurrPoint.y >= 560 || snakeCurrPoint.y < 0) { // 判断有没有撞墙
                end();
                return;
            }
            snake->setPosition(snakeCurrPoint);
            snake->setNextPoint(snakePoint);
        }  else  {
            preSnake = (SnakeSprite*)layerGameMap->getChildByTag(i - 1); // 获取蛇身体的上个节点
            preSnakePoint = preSnake->getNextPoint(); // 得到上个节点未移动前的坐标

            snake->setPosition(preSnakePoint); // 移动到上个节点的位置
            snake->setNextPoint(snakePoint); // 提供当前坐标给下个节点移动
        }
    }
    isMoveEnd = false;
}


```



有了移动就考虑吃食物和吃自己了...同样代码实现很粗暴。。。判断蛇是否吃了食物很简单,就是判断蛇的头部和食物的坐标是否一致,如果一致则代表吃到了。需要加个蛇身体节点和食物和游戏积分。
检查自己咬自己就是把自己身体的节点循环一遍,看自己的头部的坐标是否与某个身体节点的坐标是否一致。

void GameScene::check() {
    // 如果头部跟实物坐标一致则代表吃了一个食物
    if(spriteFood->getPosition() == spriteHeader->getPosition()) {
        SimpleAudioEngine::getInstance()->playEffect("eat.wav");
        addBody();
        createFood();
        gameScore += 5;
        labelScore->setString(CCString::createWithFormat("Score: %d" , gameScore)->getCString());
    }

    // 检查当前头部是否与身体节点重叠  重叠为自己咬自己  游戏结束
    for (int i = 1 ; i < bones ; i++) {
        auto spriteSnake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        if(spriteHeader->getPosition() == spriteSnake->getPosition()) {
            end();
        }
    }
}


```


创建食物:因为蛇的移动区域是760,560.蛇节点的宽高是40.所以食物生成的坐标是可以固定的。看代码

void GameScene::createFood() {
    // 760 / 40       560 / 40
    int xNumder = (19 * CCRANDOM_0_1());
    int yNumder = (14 * CCRANDOM_0_1());
    Point pointFood = Point(xNumder * 40 , yNumder * 40);
    // 防止食物生成在蛇身体节点上
    for (int i = 0 ; i < bones ; i++) {
        auto spriteSnake = (SnakeSprite*)layerGameMap->getChildByTag(i);
        if(spriteSnake->getPosition() == pointFood) {
            createFood();
            return;
        }
    }
    spriteFood->setPosition( xNumder * 40 , yNumder * 40);
}


```


貌似实现一个贪吃蛇的重点就这些了,创建蛇,移动蛇,检查撞墙,吃食物,吃自己,创建食物。

好了教程就这些了,不磨叽了,写代码还行写教程自己看了都晕。不懂请留言把。。。

没图说个屁啊:图来了。

 

   

apk文件。。

 MyCppGame.zip (1475 KB)

:2:…水中月飞天上月

果然存在问题= =

啥问题。。。

转向判断有时候会出问题

能否在详细点?:4::4::4: