用Cocos2d-x v3.0做一个贪吃蛇游戏

写在前面的废话:
在CocoaChina上看到了“进击的贪吃蛇”这个活动,翻了一下论坛也没什么人真的把教程发出来,正好今天闲的没事就写写这东西吧。自己之前一直在用2.0版本的cocos2dx,对于3.0几次想开坑学习都因为各种原因放弃掉了,版本更新太快,C++11新特性不熟等等。今天花了一天时间把这个东西搞定,才发现前面那些基本只是借口,虽然C++11的新特性啥的依旧不是太会,不过也能照猫画虎用一用v3.0了,只要努力去做,肯定有些收获的。
自己本身就是个不喜欢啰嗦的人,之前也想写过博客什么的,结果写完之后发现寥寥几句似乎就没得说了,表达能力果然还需要磨练。废话不多说,为了让我这东西能让更多的人看懂,不妨就把目标群体定位在用过v2.0的却感觉对v3.0无从下手的人吧,因此并不会解释很多基础的东西。通过这个教程,应该就会发现3.0也没那么可怕。

创建工程:
我现在用的最新版v3.0rc1的创建方式,和以前v2.2的python脚本创建方式又有了些区别,改为了通过cocos-console来创建新工程。
具体的步骤可以参考子龙山人的教学视频,这里就不再赘述了。
视频地址:http://v.youku.com/v_show/id_XNjg1Mzc4ODQ0.html

创建开始界面:

创建开始界面我们并不新建文件,直接修改HelloWorldScene.h和.cpp。
开始界面有以下几个元素:“Start Game”,“Game Help”, “Exit Game"的文字按钮,还有本身HelloWorld自带的那些东西(我删掉了关闭的按钮)。
因此我们要做的只是在原来代码的基础上添加我们的文字按钮以及设置回调函数就好。

看代码之前我们来认识v3.0和v2.0的区别之一,也就是类型名称的改动。其实很简单,只是以前带CC的东西全部去掉就好CCLayer 变成 Layer,CCSprite 变成 Sprite,仅此而已,当然也有些特例。感兴趣的可以看下:引擎中CCType.h里面写的东西。
我们来看下HelloWorldScene.h的代码:

 

#ifndef __HELLOWORLD_SCENE_H__ 
#define __HELLOWORLD_SCENE_H__ 

#include "cocos2d.h" 

USING_NS_CC; 

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

#endif // __HELLOWORLD_SCENE_H__ 


```
 

并没有修改任何代码,只是删掉了注释,没什么可多说的。不过我们有两点可以注意到: 
1.按钮的回调函数的参数类型从以前的CCobject* 变成了 Ref*。 
一般来讲,从v2.0到v3.0,类型名称的变化基本都是把开头的CC去掉即可。而CCobject算是个特例吧,直接变成了Ref。为了确认我特意试了下,并没有Object这么个类型。 
2.创建scene的函数名称从scene变成了createScene。 


然后是HelloWorldScene.cpp的代码: 


 

USING_NS_CC; 

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


```
 

createScene函数和以前比没任何变化,还是那四句。 

然后我们编写init函数。删掉原来的注释,在Point origin = Director::getInstance()->getVisibleOrigin();这句下面开始添加自己的代码 

 

//创建文字按钮 
auto labelStart = Label::create("Start Game", "宋体", 24); 
auto labelHelp = Label::create("Game Help", "宋体", 24); 
auto labelExit = Label::create("Exit Game", "宋体", 24); 

auto uiStart = MenuItemLabel::create(labelStart, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this)); 
    uiStart->setTag(1); 
    uiStart->setPosition(Point(100,200)); 
     
    auto uiHelp = MenuItemLabel::create(labelHelp, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this)); 
    uiHelp->setTag(2); 
    uiHelp->setPosition(Point(100,150)); 
     
    auto uiExit = MenuItemLabel::create(labelExit, CC_CALLBACK_1(HelloWorld::menuCloseCallback, this)); 
    uiExit->setTag(3); 
    uiExit->setPosition(Point(100,50)); 
     
    auto menu = Menu::create(uiStart,uiHelp, uiExit, NULL); 
    menu->setPosition(Point::ZERO); 
    this->addChild(menu, 1); 

```
 

我们可以看到,创建一个文本构成的ui菜单还是和以前一样,先创建文字标签,再创建对应的MenuItemLabel,最后创建Menu把MenuItemLabel都加进去,最后在让Layer添加Menu完事。 

过程仍然一样,其中也有些不可忽视的东西: 

1.auto 

我们可以看到,所有的变量创建的时候都设为了auto类型(这说法不太准确不要在意)。auto关键字用于在创建变量的时候自动判断类型,让编程更方便一些。当然也可以照我们以前写的Label labelStart这样创建变量,不过要写的东西多了点就是了。 

关于auto更深入的东西,可以参考这篇文章:点击打开链接 

2.Label 

我们之前一直用的是CCLabelTTF,理论上我们在此要使用LabelTTF,但是我在这里使用了Label。并非labelTTF被删除,“Hello World”这个文本仍然是由LabelTTF创建,可见他还存在并且可用。Label是v3.0中出现的新类型,它比以前LabelTTF更快,并且还有阴影,描边等效果。在这里我并没有用到什么label的特殊效果,不过就凭更快这点,我们也可以拿Label来替换掉所有的LabelTTF了。 

关于Label的学习,可以直接参考官方文档:http://www.cocos2d-x.org/docs/manual/framework/native/gui/label/v3/en 

3。Point 

CCPoint变成了Point这样理所当然,不过很多东西也是有了些改变。比如我们平时用的ccp(x,y)这个函数被取消,直接用构造函数来创建新的点。同时ccpAdd等运算的函数也被取消掉,通过+ - * /等运算符来替代。CC_POINT_ZERO也变成了Point::Zero。 


最后是回调函数: 
 
void HelloWorld::menuCloseCallback(Ref* pSender) 
{ 
int i = dynamic_cast(pSender)->getTag(); 
switch (i) 
{ 
case 1: 
log("go to Game"); 
//Director::getInstance()->replaceScene(GameLayer::createScene()); 
break; 
case 2: 
log("go to Help"); 
//Director::getInstance()->replaceScene(GameHelp::createScene()); 
break; 
case 3: 
case 4: 
{ 
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) 
MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert"); 
return; 
#endif 

Director::getInstance()->end(); 

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) 
exit(0); 
#endif 
} 
break; 
default: 
break; 
} 

} 


```
 


在这里我们并没有创建多个回调函数,而是在一个函数里通过判断之前给各个按钮添加的Tag从而执行不同的功能。 
写好这些代码后,build工程,点击各个文字按钮是否有效。帮助界面和游戏界面我们还没有写,所以只会出现调试信息 go to game 和go to help。 


创建帮助界面: 
 

帮助界面元素:返回主菜单的文字按钮,中央的说明文字。 
帮助界面并没有什么新东西,写这个部分的代码就权当复习上面的东西了。 
GameHelp.h: 

 

#ifndef __Snake__GameHelp__ 
#define __Snake__GameHelp__ 

#include  
#include "cocos2d.h" 

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

#endif /* defined(__Snake__GameHelp__) */ 


```
 

除了类型的名称和回调函数名不一样,其余均和HelloWorldScene.h中的一致。 




GameHelp.cpp 

 

#include "GameHelp.h" 
#include "HelloWorldScene.h" 

USING_NS_CC; 

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

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

//创建帮助文字 
auto labelHelp = Label::create("Please click screen to start the game!", "宋体", 24); 
labelHelp->setPosition(Point(480,320)); 
this->addChild(labelHelp); 

//创建返回按钮 
auto labelBack = Label::create("MainMenu", "宋体", 24); 
auto uiBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameHelp::menuBackToMain, this)); 
uiBack->setPosition(Point(100,50)); 

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

return true; 

} 

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

```
 




创建两个元素的方式也和之前一样,没有什么可太多说明的。需要注意的是回调函数这里,我们之前惯用的CCDirector::sharedDirector变成了getInstance,除了名称有变化,功能还是一样的。而且,在v2.2.3里面也变成了这样子。 
写完代码之后,在HelloWorldScene.cpp中包含GameHelp.h并且把回调函数中case2 的注释变成代码。build后看是否能进行界面的顺利跳转。 


创建游戏界面: 
 

游戏界面的元素:蛇的身体、蛋、网格 
终于到了重头戏。这个贪吃蛇游戏比较简单,只有蛇和蛋还有网格,没有提供记分系统。机制上也并没有常见的加速,撞墙,咬到自己也不会死,可以说是去掉了很多机制的极简版贪吃蛇。感兴趣的读者可以自己加上各种机制,也并不太难,就看大家的精力了。 
我们先看GameLayer.h: 



 

enum DIR_DEF 
{ 
UP = 1, 
DOWN, 
LEFT, 
RIGHT 
}; 
class SnakeNode: public cocos2d::Ref 
{ 

public: 
DIR_DEF dir; 
int row, col; 
}; 

class GameLayer: public cocos2d::Layer 
{ 
private: 

SnakeNode* m_head; 
SnakeNode* m_food; 
cocos2d::Vector m_body; 

public: 
virtual bool init(); 
virtual void draw(cocos2d::Renderer *renderer, const kmMat4 &transform, bool transformUpdated); 
void update(float dt); 
static cocos2d::Scene* createScene(); 
CREATE_FUNC(GameLayer); 
void menuBackToMain(cocos2d::Ref *pSender); 

virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event); 

}; 


```
 

在这里我们多定义了一个类SnakeNode,用来表示画面中的元素(蛇头、蛇的身体、还有蛋都算),因为只有变量,并没有方法和实现,所以没有多建立一个文件。 

GameLayer中的大部分和之前的都差不多,值得注意的有两点: 

1.draw函数 

因为我们并没有提供任何额外的图片,所以我们所有的绘制都要重载draw函数从而调用基本的绘图函数来绘制游戏场景。在之前我们一直用virtual void draw()来重载draw函数,但是在3.0里没有任何参数的draw函数被设为了final,即无法重载的,取而代之的是代码中的virtual void draw(Renderer* renderer, const kmMat4 &transform, bool transformUpdated),多了很多参数,但是用法其实还是和以前一样。我们只需要把我们的绘制函数写进去,最后再 

调用父类的draw函数即可。 




2.Vector 

在v3.0中已经不推荐使用我们常用的CCArray,取而代之的是Vector,类似于我们在C++中经常用的std::Vector。在我看来最大的优势在于声明了元素类型,从而不用把每个取出来的元素都进行类型转换,方便了很多也安全了很多。 




GameLayer.cpp 
 

#include "GameLayer.h" 
#include "HelloWorldScene.h" 

USING_NS_CC; 

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

bool GameLayer::init() 
{ 
if(!Layer::init()) 
{ 
return false; 
}  
auto labelBack = Label::create("MainMenu", "宋体", 24); 
auto uiBack = MenuItemLabel::create(labelBack, CC_CALLBACK_1(GameLayer::menuBackToMain, this)); 
uiBack->setPosition(Point(900,200)); 

auto menu = Menu::create(uiBack, NULL); 
menu->setPosition(Point::ZERO); 
this->addChild(menu); 
//随机初始化蛇以及蛋 
m_head = new SnakeNode(); 
m_head->row = arc4random()%10; 
m_head->col = arc4random()%10; 

m_food = new SnakeNode(); 
m_food->row = arc4random()%10; 
m_head->col = arc4random()%10; 
//建立触摸监听 
auto listener1 = EventListenerTouchOneByOne::create(); 
listener1->setSwallowTouches(true); 
listener1->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan, this); 

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this); 

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

return true; 
} 

```
 

createScene和之前一样不再赘述。在Init函数中我们可以看到,Vector并不需要像CCArray那样进行create,只需要创建就好。另外,我们看到了一个新的东西Listener,这个是v3.0中新的触摸机制,取代了之前的setTouchEnabled等等触摸机制。 


新的触摸机制: 

新的触摸机制采取了类似于flash中的listener,每次发生相应的时间就执行对应的函数,它不光可以像以前那样挂在Layer上,也可以挂在Sprite等等东西上。这样就不用再有满天飞的如何写一个可接受触摸的Sprite类等等的问题了。 

实现新的触摸机制我们需要以下几步: 

1.创建新的EventListenner,EventListener不光是接受触摸的,键盘什么的也改成了这种机制,不过这里我们只讨论触摸。 

auto listener1 = EventListenerTouchOneByOne::create(); 

auto listener2 = EventListnereTouchAllatOnce::create(); 

这两种类型分别对应了单点触摸和多点触摸。相比以前的多点触摸用setTouchEnabled,单点用触摸代理注册的混乱情形有了很大改善。 

2.设置触摸的回调函数。 

这个和以前基本一样,还是分为了began,moved,ended和canceled四种,只不过我们需要手动选择在这些事件发生时他们要加载的函数 

listener1->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan,this); 

一般的教程都直接把函数写在了等号后面并没有新建函数,这里给出了已经写出函数后调用的方法。 

3.挂载监听 

_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1,this); 

_eventDispatcher是节点自带的触摸管理器,通过addEventListenerWithSceneGraphPriority可以向对象上挂载监听,第二个参数可以是某个Sprite甚至是label等等。触摸的优先级则是根据图片显示的顺序而来,越在上面的优先级越高。如果之前设置了SwallowTouches为true那么会吞噬掉优先度低的触摸。 




关于进一步的触摸机制说明,参见官方文档:http://www.cocos2d-x.org/docs/manual/framework/native/input/event-dispatcher/en 




让我们接着看下面的代码: 




 

void GameLayer::draw(cocos2d::Renderer *renderer, const kmMat4 &transform, bool transformUpdated) 
{ 
//绘制直线 
glLineWidth(4); 
for (int i = 0; i<11; i++) 
{ 
DrawPrimitives::drawLine(Point(0,i*64), Point(640,i*64)); 
DrawPrimitives::drawLine(Point(i*64,0), Point(i*64,640)); 
} 

//绘制蛇头 
DrawPrimitives::drawSolidRect(Point(m_head->col * 64 , m_head->row * 64 ), 
Point(m_head->col * 64 +64,m_head->row * 64 +64), 
Color4F(1.0f,0,0,1.0f)); 

//绘制蛋 
DrawPrimitives::drawSolidRect(Point(m_food->col * 64 , m_food->row * 64 ), 
Point(m_food->col * 64 +64,m_food->row * 64 +64), 
Color4F(0,0,1.0f,1.0f)); 

//绘制蛇身 
for(auto &sn : m_body) 
{ 
DrawPrimitives::drawSolidRect(Point(sn->col * 64, sn->row * 64), 
Point(sn->col * 64 + 64, sn->row * 64 +64), 
Color4F(0,0,1.0f,1.0f)); 
} 

Layer::draw(renderer, transform, transformUpdated); 
} 

void GameLayer::update(float dt) 
{ 
//蛇身每一段跟随前一段移动 
for(int i = m_body.size() -1 ;i>=0;i--) 
{ 
SnakeNode* sn = m_body.at(i); 

if(i!=0) 
{ 
SnakeNode* pre = m_body.at(i-1); 
sn->dir = pre->dir; 
sn->col = pre->col; 
sn->row = pre->row; 
} 
else 
{ 
sn->dir = m_head->dir; 
sn->col = m_head->col; 
sn->row = m_head->row; 
} 
} 

//根据方向来让蛇头移动 
switch (m_head->dir) 
{ 
case UP: 
m_head->row++; 
log("up"); 
if(m_head->row >=10) m_head->row = 0; 
break; 
case DOWN: 
m_head->row--; 
log("down"); 
if(m_head->row <0) m_head->row = 9; 
break; 
case RIGHT: 
m_head->col++; 
if(m_head->col >=10) m_head->col = 0; 
break; 
case LEFT: 
m_head->col--; 
if(m_head->col < 0) m_head->col = 9; 
break; 
default: 
break; 
} 

//和蛋的碰撞检测 
if(m_head->row == m_food->row && m_head->col == m_food->col) 
{ 
//刷新蛋 
m_food->row = arc4random()%10; 
m_food->col = arc4random()%10; 

//设置新的尾巴的参数 
SnakeNode* sn = new SnakeNode(); 
SnakeNode* last = NULL; 
if(m_body.size() > 0) 
{ 
last = m_body.back(); 
} 
else 
{ 
last = m_head; 
} 

switch (last->dir) 
{ 
case UP: 
sn->row = last->row - 1; 
sn->col = last->col; 
break; 
case DOWN: 
sn->row = last->row + 1; 
sn->col = last->col; 
break; 
case LEFT: 
sn->row = last->row; 
sn->col = last->col + 1; 
break; 
case RIGHT: 
sn->row = last->row; 
sn->col = last->col - 1; 
break; 

default: 
break; 
} 
//添加进身体的Vector中 
m_body.pushBack(sn); 
} 
} 

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

bool GameLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event) 
{ 
Point tPos = touch->getLocation(); 
int nowRow = ((int)tPos.y)/64; 
int nowCol = ((int)tPos.x)/64; 

if(abs(nowRow - m_head->row) > abs(nowCol - m_head->col)) 
{ 
//上下移动 
if(nowRow > m_head->row) 
{ 
m_head->dir = UP; 
} 
else 
{ 
m_head->dir = DOWN; 
} 
} 
else 
{ 
//左右移动 
if(nowCol > m_head->col) 
{ 
m_head->dir = RIGHT; 
} 
else 
{ 
m_head->dir = LEFT; 
} 
} 

return true; 
} 


```
 


代码很长,我们一一来看。 
首先是menu的回调函数,和之前一样,没什么好说的。
接下来是触摸的回调函数onTouchBegan,依旧是touch->getLocation(),之后再去写逻辑这样的用法,可以说和以前一模一样,完全无须担心什么。
再来看draw函数,多了一些参数其实并没有用上。基本的绘制函数还是和以前一样,只不过每个函数之前都要加上DrawPrimitives::这个命名空间了,不知道是不是我xcode的问题,不加这个它就报错。
最后我们来看update函数。算法很清晰,我觉得做过一两个游戏的人都能简单的看懂。我们来简要的说些Vector的用法,Vector基本相当于std::Vecot所以之前CCArray那种和NSArray风格的函数都改头换面了。addObject变为pushBack,objectAtIndex变为at,lastObject变为back,count变为size。基本上换汤不换药,还多了一些find,insert等等的功能。以前需要retain,现在也没什么必要了。
不过Vector也并没有那么简单,内存的申请什么的参考官方文档:http://www.cocos2d-x.org/docs/manual/framework/native/data-structure/v3/vector/en
关于各种ptr什么的我理解并没有那么清晰,所以在代码中也没有这么做,关于这方面还请各位大大指教。


写在最后
文章写到这里就差不多了,相信诸位也和我一样初步理解了v3.0基础的一些东西,可以用v3.0做一些小东西出来了。整体文章对贪吃蛇的算法并没有任何的教学,因为我认为有过一些经验的人这并不是什么难事,而且直接看代码也可以看懂。文章的重点还是在于从2.0怎么开始学习3.0上,因此多了很多的参考资料,更像是一个各类文章的不完全集合贴,希望给之前和我一样迷茫的人一些帮助吧。第一次写这么长的教程,可能并没有很多人看得懂,还请各位海涵。


ipa:

谢谢分享,好东西!
我现在也还在用v2.2.2
以后稳定再看看3.0…

楼主干得漂亮!32个赞!喜欢楼主这句,前面的都是借口。。。只要努力去做,肯定有些收获的!

版主没回我的帖子。。我更早呢。。:10:

牛逼 动作这么快 我3.0还没下好呢

请看,右上角私信,按照活动规则进行哦。

楼主牛掰闪闪,我给你的帖子发到CocoaChina微博去了。楼主去转发下吧。

啊~~~是我搞错规则了

楼主,好厉害

感谢楼主奉献,,:14:

楠楠 你真的的头像才是牛掰闪闪好么:11::11::11:

好长的文章呀 看来LZ花了不少时间呀

支持楼主~~~~~

:2: :2: :2: :2: :2: :2:学习,谢谢分享

您的文章已被推荐到CocoaChina首页热门文章精选

— Begin quote from ____

引用第2楼偶尔e网事于2014-04-11 10:06发表的 :
楼主干得漂亮!32个赞!喜欢楼主这句,前面的都是借口。。。只要努力去做,肯定有些收获的! http://www.cocoachina.com/bbs/job.php?action=topost&tid=197630&pid=924158

— End quote

这个帖子必须加精华!你怎么看!

:904:
炒鸡棒~看到3.0的教程~默默的留下爪印~

楼主请教一下,为什么我在创建菜单的时候,
–写完代码之后,在HelloWorldScene.cpp中包含GameHelp.h并且把回调函数中case2 的注释变成代码。build后看是否能进行界面的顺利跳转。–
HelloWorldScene.cpp中会出现无法打开“GameHelp.h”
但是GameHelp.cpp却可以 include“HelloWorldScene。h”

在HelloWorldScene无法写 #include “GameHelp.h”

出现 HelloWorldScene.cpp中会出现无法打开“GameHelp.h”

楼主可以发份 源码 吗 谢谢了

2425288345@qq.com