写在前面的话:
我自己其实也只是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)
回头俺也写一个