怎么用Cocos2d-x v3.0做一个贪吃蛇

写在前面的话:

我自己其实也只是cocos2dx的一个初学者,接触cocos2dx大概只有半年的时间。也算是刚刚掌握cocos2dx的一些基本功能吧。前段时间cocos2dx3.0发布了,我也有研究了一下新版本的变化。而这次刚好看到论坛里有这个进击的贪吃蛇的活动,于是也打算来试一下。顺便也复习一下前段时间学习的内容。

由于作者的水平有限,于是整个代码基本都是参考了样章里带的贪吃蛇的源码,把它转化成cocos2dx 3.0版本的,但是有一个地方做了改变。

样章里的源码中,整个游戏界面都是靠opengl直接绘制点,线,多边形画出来的。而作者对这部分内容不是很熟悉。平时的游戏项目里面也基本是用Sprite,很少接触opengl的直接绘制。所以我把这部分内容换成了用Sprite来实现,蛇的身体和食物分别使用蓝色和黄色的圆点图片来表示。

最后,本教程是使用cocos2dx 3.0rc0 实现的,由于是想直接等稳定版所以作者没有去下载rc1,请大家注意。那么下面就开始吧。

一.开始写游戏之前

在我们正式写代码之前,先要搞清楚,我们要写的游戏是一个什么样的游戏。游戏一共有几个场景,每个场景大概是什么样子,玩家要怎样操作游戏等等很多相关的问题要先有个大概的构想,真正开始写的时候思路才会比较清晰。

由于这部分跟代码关系不大,而且贪吃蛇属于经典的老游戏,规则大家都很熟悉了,并且我们也不打算做太多的扩展功能。所以这部分就先略过了,我就直接把结果说出来。

游戏一共有三个场景。玩家一打开游戏是一个开始界面,这里可以选择开始游戏,游戏帮助和退出游戏。

点击游戏帮助,就会进入帮助的场景。里面可以写一些操作说明之类的内容。
点击开始游戏,就进入游戏场景。然后就是经典的贪吃蛇游戏的规则。

由于游戏实现的比较简陋,所以并没有写死亡的逻辑,所以也就没有游戏结束界面。同时游戏里也没有计分,所以也没有查看历史分数之类的界面。如果大家有兴趣可以尝试自己实现这部分的内容。

二.完成游戏的框架

想好了我们的游戏是什么样子了之后,我们就要开始正式来写我们的游戏了。一个游戏要从什么地方开始写,每个人的习惯不一样。这里我就按我自己的习惯来了,我比较喜欢先完成游戏的框架,把每个场景和场景的跳转先完成,然后再来写每个场景里面具体是什么样子的。

首先,我们需要新建一个游戏项目。在cocos2dx 3.0里面创建项目的方式又发生了变化。但是无论是官网还是论坛里,相关的资料都很多了,我这里就不再详细说明了。

游戏项目创建好了之后,会默认生成HelloWorldScene的头文件和cpp文件。这时候直接运行就可以看到这个HelloWorldScene是什么样子的。

由于我们整个游戏的逻辑非常简单,所以我们可以只修改这两个文件来实现我们的游戏。如果游戏的逻辑比较复杂,还是建议大家分成多个文件来实现自己的游戏,也方便代码的管理。

我们先来看helloworld这个类的头文件。(为了方便,我把之前代码的注释都去掉了)

class HelloWorld : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();  
    void menuCallback(cocos2d::Ref* pSender);
    CREATE_FUNC(HelloWorld);
};


```


这里我们可以看到HelloWorld里自动生成的几个方法。跟cocos2dx 2.x版本基本没有变化,对于没有接触过cocos2dx的人来说,从方法的名字上也基本可以理解每个方法的作用。(menuCloseCallback的方法,因为在后面要改写,功能发生的变化,所以名字被我改成了menuCallback)

不过这里要注意一下cocos2dx 3.0里面命名方式与2.x相比发生的变化。

在3.0里,废除了之前以CC开头的命名方式。所有的CC开头的类都去掉了CC。比如CCLayer,CCScene分别变为了Layer和Scene。

另外,以前的CCObject因为名字表意不清,替换为Ref,ref是引用计数的缩写。Cocos2dx里面所有继承Ref的类都采用了引用计数的功能,由自动释放池来自动管理内存。如果我们自己写的类也想用这个内存管理机制,也可以继承Ref。

对于新人,可以暂时放下这部分内容,如果感兴趣的话,可以自己搜索一下cocos2dx的内存管理机制,网上有很多相关的资料。


仿照HelloWorld的定义,我们可以在头文件里面先把我们游戏的另外两个场景先定义出来。
class GameHelp : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();  
    void menuBackToMain(cocos2d::Ref* pSender);
    CREATE_FUNC(GameHelp);
};

class GameScene : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();  
    void menuBackToMain(cocos2d::Ref* pSender);
    CREATE_FUNC(GameScene);
};


```


接下来,我们来看代码的具体实现。

关于创建场景的方法createScene,这部分内容基本没有什么变化。我们每个场景都可以照搬HelloWorld,只需要把HelloWorld换成自己定义的类。

在HelloWorld的create方法里会执行下面的init;这里面则是初始化我们的场景。

默认的情况下,init里面有自带的一些代码。不过现在我们要把它改成我们自己的场景。我先给出我的HelloWorld的init。

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Point origin = Director::getInstance()->getVisibleOrigin();

    auto labelStart = Label::create("StartGame", "Arial", 24);
    auto labelHelp = Label::create("GameHelp", "Arial", 24);
    auto labelExit = Label::create("ExitGame", "Arial", 24);

    auto startItem = MenuItemLabel::create(labelStart, CC_CALLBACK_1(HelloWorld::menuCallback, this));
    startItem->setTag(START);
    startItem->setPosition(Point(100, 200));

    auto helpItem = MenuItemLabel::create(labelHelp, CC_CALLBACK_1(HelloWorld::menuCallback, this));
    helpItem->setTag(HELP);
    helpItem->setPosition(Point(100, 150));

    auto exitItem = MenuItemLabel::create(labelExit, CC_CALLBACK_1(HelloWorld::menuCallback, this));
    exitItem->setTag(EXIT);
    exitItem->setPosition(Point(100, 50));

    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCallback, this));
    
    closeItem->setPosition(Point(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                                origin.y + closeItem->getContentSize().height/2));
    closeItem->setTag(EXIT);
    auto menu = Menu::create(startItem, helpItem, exitItem, closeItem, NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu, 1);

    auto label = Label::create("Snake", "Arial", 24);
    label->setPosition(Point(origin.x + visibleSize.width/2,
                            origin.y + visibleSize.height - label->getContentSize().height));
    this->addChild(label, 1);

    return true;
}


```


这部分代码虽然比较长,但其实是非常简单的,大多是繁琐重复的工作。

场景里面我加了三个标签菜单项,分别是开始游戏,游戏帮助和退出游戏。
 
创建的时候要先创建标签,设定标签的文字,字体,字号。然后用标签来创建标签菜单项,并指定如果标签被按下要执行的回调函数。


因为三个标签菜单项我都指定了同一个回调函数,为了区分它们,我把三个标签都加了tag 。

Tag虽然可以直接写1,2,3,但是为了代码的可读性,我在HelloWorld的头文件里定义了一个枚举类。

typedef enum {
    START=10,
    HELP,
    EXIT
}TAG_MENU;


```



设定了tag之后就是设定标签的位置,位置直接写坐标就可以了。
 
整个游戏我是按照 480 320的分辨率设计的。关于分辨率的适配也是一个麻烦的工作,这个游戏里我就不做适配了,就按照最简单的,以480 320的分辨率直接缩放到全屏。稍后我会说明这个要怎么做。
 
创建好了菜单项,就要创建一个菜单,把菜单项放进去。默认的helloworld里面就有一个菜单,里面只有那个关闭游戏的按钮,我们把我们新加的菜单项也一起加进去。
 
最后要把菜单加到场景中,不然菜单里的内容不会显示。
 
默认的代码里面自带的背景图片我去掉了,关闭按钮的回调函数也修改了名字,并设置了tag。
 
总的来说,整个代码其实非常的简单,如果以前用过cocos2dx 2.x,肯定是一下就看懂了。
 
这里要注意的几个3.0版的变化是:
3.0版里,单例模式取消了之前sharedXXX的命名方法,统一变成了getInstance。比如之前的sharedDirector变成了getInstance.
3.0版里增加了新的Label,以前的Label虽然也可以用,但是新的Label功能更强大,只不过这里体现不出来。
3.0版里回调函数都统一使用了CC_CALLBACK_x的形式,x可以是0,1,2,3,回调函数有几个参数,就用几。这里面因为menuCallback方法只有一个参数,所以用CC_CALLBACK_1

对于menuCallback,默认的是一个退出游戏的操作。这里因为我们多了几个菜单项,上面设置的tag就发挥作用了。我们可以获取到用户点击的对象的tag,然后从tag来判断用户点击了哪个菜单项。如果点击的是退出,就执行默认代码里的操作。如果点击另外两个按钮,就由Cocos2dx自带的导演类来负责切换场景,这个导演类采用了单例模式。
void HelloWorld::menuCallback(Ref* pSender)
{
    switch (((Node*)pSender)->getTag())
    {
    case START:
        CCLOG("go to game");
        Director::getInstance()->replaceScene(GameScene::createScene());
        break;
    case  HELP:
        CCLOG("go to help");
        Director::getInstance()->replaceScene(GameHelp::createScene());
        break;
    case EXIT:
        Director::getInstance()->end();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
        exit(0);
#endif
    }
}


```


仿照helloworld,我们可以把GameHelp界面也写出来,GameScene也可以先写个框架,不涉及游戏的实现。下面我把代码贴出来,就不解释了。

bool GameHelp::init()
{
    if (!Layer::init())
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();

    auto labelHelp = Label::create("please touch screen to play the game", "Arial", 15);
    labelHelp->setPosition(Point(visibleSize.width / 2, 280));
    labelHelp->setAnchorPoint(Point::ANCHOR_MIDDLE);
    this->addChild(labelHelp);

    auto labelBack = Label::create("Main Menu", "Arial", 15);
    auto itemBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameHelp::menuBackToMain, this));
    itemBack->setPosition(Point(visibleSize.width / 2, 160));

    auto menu = Menu::create(itemBack, NULL);
    menu->setPosition(Point(0, 0));
    this->addChild(menu);

    return true;
}

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

void GameHelp::menuBackToMain( cocos2d::Ref* pSender )
{
    Director::getInstance()->replaceScene(HelloWorld::createScene());
}

bool GameScene::init()
{
    if (!Layer::init())
    {
        return false;
    }

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

    auto labelBack = Label::create("Main Menu", "Arial", 15);
    auto itemBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameScene::menuBackToMain, this));
    itemBack->setPosition(Point(visibleSize.width, 0));
    itemBack->setAnchorPoint(Point::ANCHOR_BOTTOM_RIGHT);

    auto menu = Menu::create(itemBack, NULL);
    menu->setPosition(Point(0, 0));
    this->addChild(menu);

    return true;
}

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

void GameScene::menuBackToMain( cocos2d::Ref* pSender )
{
    Director::getInstance()->replaceScene(HelloWorld::createScene());
}


```


这部分没有涉及什么新内容,只是多了anchor point也就是锚点的概念。这个锚点是图片或者标签的基准点。比如我想把一张图片放到屏幕右下角,那么这张图片的什么位置要放在右下角呢?如果我把锚点设置为图片的右下角,那结果就是图片的右下角和屏幕的右下角重合,于是我们就看到整张图片刚好在屏幕右下角。

到这里,我们框架就算搭建好了。我们可以先试着运行一下。不过,在运行之前,我们先要设置一下我们的分辨率方案,在AppDelegate.cpp里面,加一行代码,指定设计分辨率。指定适配方案为Exact Fit,这样游戏就会以 480 320为分辨率,缩放到整个屏幕。

bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLView::create("My Game");
        director->setOpenGLView(glview);
    }
    glview->setDesignResolutionSize(480, 320, ResolutionPolicy::EXACT_FIT);
    // turn on display FPS
    director->setDisplayStats(false);
...
}


```




三 设计游戏逻辑

游戏框架搭建好了之后,就要具体实现我们的游戏逻辑了。因为这一部分很多是C++的内容,跟Cocos2dx关系不大,所以一部分解释的内容会省略掉。


首先,在HelloWorld的头文件里,我们要新定义一个枚举类,来表示蛇运动的方向,同时定义一个类表示蛇的节点。

typedef enum {
    UP=1,
    DOWN,
    LEFT,
    RIGHT
}DIR_DEF;

class SnakeNode :public cocos2d::Ref
{
public:
    int row;
    int col;
    int dir;
    SnakeNode* preNode;
    cocos2d::Sprite* nodeSprite;
};


```


另外,在游戏场景里面,我们还要再增加一些方法和属性。

class GameScene : public cocos2d::Layer
{
public:
    virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);
    void logic(float t);
    static cocos2d::Scene* createScene();
    virtual bool init();  
    void menuBackToMain(cocos2d::Ref* pSender);
    CREATE_FUNC(GameScene);
    
protected:
    SnakeNode* sHead;
    SnakeNode* sFood;
    cocos2d::Vector allbody;
};


```



这里要注意一点,在3.0版本里面,废除了之前的CCArray,而采用C++自己的Vector,cocos2dx稍微给它包装了一下,但是使用方法跟以前还是有很大区别。

接下来在场景初始化里面需要新增加一些内容。

bool GameScene::init()
{
    if (!Layer::init())
    {
        return false;
    }

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

    auto labelBack = Label::create("Main Menu", "Arial", 15);
    auto itemBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameScene::menuBackToMain, this));
    itemBack->setPosition(Point(visibleSize.width, 0));
    itemBack->setAnchorPoint(Point::ANCHOR_BOTTOM_RIGHT);

    auto menu = Menu::create(itemBack, NULL);
    menu->setPosition(Point(0, 0));
    this->addChild(menu);

    sHead = new SnakeNode();
    sHead->row = rand()%10;
    sHead->col = rand()%15;
    sHead->preNode = NULL;

    sFood = new SnakeNode();
    sFood->row = rand()%10;
    sFood->col = rand()%15;

    auto snakeNode = Sprite::create("node_snake.png");
    snakeNode->setAnchorPoint(Point::ANCHOR_BOTTOM_LEFT);
    snakeNode->setPosition(Point(sHead->col * 32, sHead->row * 32));
    sHead->nodeSprite = snakeNode;
    this->addChild(snakeNode);

    auto foodNode = Sprite::create("node_food.png");
    foodNode->setAnchorPoint(Point::ANCHOR_BOTTOM_LEFT);
    foodNode->setPosition(Point(sFood->col * 32, sFood->row * 32));
    sFood->nodeSprite = foodNode;
    this->addChild(foodNode);

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->setSwallowTouches(true);

    touchListener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan, this);

    Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(touchListener, this);

    this->schedule(SEL_SCHEDULE(&GameScene::logic), 0.5f);
    return true;
}


```



首先,我事先准备了node_snake.png和node_food.png放在了Resources目录下面。这两张图片就是蓝色和黄色的一个圆点,大小是32 * 32的。所以480 * 320 的屏幕刚好可以分成 15 * 10的格子。每个snakeNode的row和col就是记录这个节点在哪一行哪一列。初始化场景的时候,会随机生成初始的蛇头和食物位置。


然后,游戏要通过点击屏幕来控制,所以需要注册屏幕点击的事件。这里3.0版本跟之前也有很大不同。


首先,注册的方法发生了变化,具体的可以参考上面的代码。
另外,注册的位置也不一样了。以前是在onEnter里注册,在onExit里面注销。现在是在init里面注册。并且不需要我们管理注销,由node来管理。如果我们不是在node里面注册的事件,还是要我们自己来管理注销。


schedule方法还是一样,可以设定程序每一帧都执行某个代码,或者每隔多少时间执行一次某个代码。只是写法有少许变化。


最后的屏幕点击事件,还有logic方法就完全是C++的内容了,Cocos2dx的部分之前都已经解释过了,所以这里就只把代码放上来,不再做详细的解释了。


bool GameScene::onTouchBegan( Touch* touch, Event* event )
{
    Point touchPoint = touch->getLocation();
    int touchRow = ((int)touchPoint.y) / 32;
    int touchCol = ((int)touchPoint.x) / 32;

    if (abs(touchRow - sHead->row) > abs(touchCol - sHead->col))
    {
        if (touchRow > sHead->row)
        {
            sHead->dir = DIR_DEF::UP;
        } 
        else
        {
            sHead->dir = DIR_DEF::DOWN;
        }        
    }
    else
    {
        if (touchCol > sHead->col)
        {
            sHead->dir = DIR_DEF::RIGHT;
        } 
        else
        {
            sHead->dir = DIR_DEF::LEFT;
        }        
    }
    
    return true;
}

void GameScene::logic( float t )
{
    for (int i = allbody.size() - 1; i >= 0; i--)
    {
        auto sn = allbody.at(i);
        sn->dir = sn->preNode->dir;
        sn->row = sn->preNode->row;
        sn->col = sn->preNode->col;
        sn->nodeSprite->setPosition(Point(sn->col * 32, sn->row *32));
    }
    
    switch (sHead->dir)
    {
    case DIR_DEF::UP:
        sHead->row ++;
        if (sHead->row >= 10)
        {
            sHead->row = 0;
        }
        break;
    case DIR_DEF::DOWN:
        sHead->row --;
        if (sHead->row <= 0)
        {
            sHead->row = 9;
        }
        break;
    case DIR_DEF::LEFT:
        sHead->col --;
        if (sHead->col <= 0)
        {
            sHead->col = 14;
        }
        break;
    case DIR_DEF::RIGHT:
        sHead->col ++;
        if (sHead->col >= 15)
        {
            sHead->col = 0;
        }        
        break;
    }
    sHead->nodeSprite->setPosition(Point(sHead->col * 32, sHead->row * 32));

    if (sHead->col == sFood->col && sHead->row == sFood->row)
    {
        sFood->row = rand()%10;
        sFood->col = rand()%15;
        sFood->nodeSprite->setPosition(Point(sFood->col * 32, sFood->row * 32));
        auto snakeNode = new SnakeNode();
        if (allbody.empty())
        {
            snakeNode->preNode = sHead;
        }
        else
        {
            snakeNode->preNode = allbody.back();
        }
        switch (snakeNode->preNode->dir)
        {
        case DIR_DEF::UP:
            snakeNode->row = snakeNode->preNode->row - 1;
            snakeNode->col = snakeNode->preNode->col;
            break;
        case DIR_DEF::DOWN:
            snakeNode->row = snakeNode->preNode->row + 1;
            snakeNode->col = snakeNode->preNode->col;
            break;
        case DIR_DEF::LEFT:
            snakeNode->row = snakeNode->preNode->row;
            snakeNode->col = snakeNode->preNode->col + 1;
            break;
        case DIR_DEF::RIGHT:
            snakeNode->row = snakeNode->preNode->row;
            snakeNode->col = snakeNode->preNode->col - 1;
            break;
        }

        auto nodeSprite = Sprite::create("node_snake.png");
        nodeSprite->setPosition(Point(snakeNode->col * 32, snakeNode->row * 32));
        nodeSprite->setAnchorPoint(Point::ANCHOR_BOTTOM_LEFT);
        snakeNode->nodeSprite = nodeSprite;
        this->addChild(nodeSprite);

        allbody.pushBack(snakeNode);
    }
    
}


```



这里唯一要注意的地方是,3.0里面新增了图片资源autoBatch的功能。不需要我们再像以前那样需要自己写一个batchNode。所以虽然蛇后面每吃到一个食物都会增加一个节点,同时增加一个节点的sprite,但是这些sprite会自动batch到一起,一次绘制出来。


另外,游戏里面没有加死亡的逻辑,感兴趣的可以自己补上这部分内容。


到这里,整个教程就结束了,感觉不知不觉就写了好多。希望能对新人们有所帮助,尽快上手cocos2dx 3.0 。 最后就放上几张游戏截图还有apk吧
  
  
 Snake.zip (1542 KB)

不错,速度联系版主!!!

:801::801::801:32个赞,逻辑相当清楚!

:2: :2: :2: :2: :2: :2: 赞,大赞!!

又一篇教程来了

:14: 回头俺也写一个

在eclipse怎么新建头文件啊,说classes不是源目录

支持楼主,定

:14::14::14::14::14:

怎么在苹果电脑上打开apk文件呢- -!

mark!!!

支持楼主:2:

按照步骤实现了贪吃蛇,楼主的教材非常详细,十分感谢

请问楼主是用eclipse 还是 vs来编译?

再度想问问,假如我要增加初始长度应该怎么改,加什么,新手求指导

觉得一开始增加节点就好啦