Cocos2d实战之别踩白块

Cocos2d-x 别踩白块


前言

将很久以前写的教程梳理梳理



原文

环境:

  • Cocos2d-x 3.2
  • VS2012

块,Block

首先新建一个项目,

然后修改它的 AppDelegate.cpp ,将显示尺寸调整成竖屏分辨率。

最后在生成APK前,还要在android那设置,永远竖屏的属性。

最重要的类:块,Block

在游戏内,有很多不同颜色的块(黑色、白色、黄色等),这些块统一封装到一个类中,通过不同参数来控制呈现不同状态。

参数:

  • 颜色
  • 大小
  • 内容文本
  • 内容字体大小
  • 内容字体颜色

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Block.cpp

bool Block::init( Color3B color , Size size , std::string label , float fontSize , Color4B textColor )
{
// 先执行Sprite的初始化
if( !Sprite::init() ) {
return false;
}

//设置Block的内容大小
setContentSize(size);
//左下角对齐
setAnchorPoint(Point::ZERO);
//文字显示位置
setTextureRect(Rect(0,0,size.width,size.height));
//颜色
setColor(color);
//添加一个文本
auto l = Label::create();
l->setString(label);
l->setSystemFontSize(fontSize);
l->setTextColor(textColor);
//添加到当前场景中
addChild(l);
l->setPosition(size.width/2,size.height/2);

return true;
}

然后,在Block类中,还需要支持增删方法及容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Block.cpp

// 初始化Vector
Vector<Block*> * Block::blocks = new Vector<Block*>();
Block* Block::createBlock( Color3B color , Size size , std::string label , float fontSize , Color4B textColor )
{
auto block = new Block();
block -> init(color,size,label,fontSize,textColor);
block -> autorelease();

blocks -> pushBack(block);

return block;
}

void Block::removeBlock()
{
removeFromParent();
blocks->eraseObject(this);
}

基础的块类实现后,来贴合游戏业务,实现开始行和结束行。

此处,直接调整模板附带的HelloWorldScene

1
2
3
4
5
6
7
8
// HelloWorldScene.cpp

void HelloWorld::addStartLine()
{
//添加一个方块
auto block = Block::createBlock(Color3B::YELLOW,Size(visibleSize.width,visibleSize.height/4),"",20,Color4B::BLACK);
addChild(block);
}

添加完开始行,看一下效果:

添加结束行

1
2
3
4
5
6
7
8
// HelloWorldScene.cpp

// 添加结束行
void HelloWorld::addEndLine()
{
auto block = Block::createBlock(Color3B::GREEN,visibleSize,"Game Over",32,Color4B::BLACK);
addChild(block);
}

添加完开始行和结束行,再将中间内容黑白块填充一下

首先,在Block类中添加一个变量 lineIndex,

然后,添加与它相关的get和set函数

这个变量用处是 来知道该块放在哪一行,

比如开始行就应该放在第0行(在addStartLine相应位置修改一下)

结束行就该放在 第4行(同上,也在相应位置修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HelloWorldScene.cpp

// 添加黑白块
void HelloWorld::addNormalLine( int lineIndex )
{
Block* block;
//rand()是伪随机数
int blackIndex = rand()%4;
for (int i = 0; i < 4; i++)
{
block=Block::createBlock(blackIndex==i?Color3B::BLACK:Color3B::WHITE,
Size(visibleSize.width/4-2,visibleSize.height/4-2),"",20,Color4B::BLACK);
addChild(block);

block->setPosition(i*visibleSize.width/4, lineIndex*visibleSize.height/4);
block->setLineIndex(lineIndex);
}
}

要知道,整个界面被分成四行(0、1、2、3) 和 四列,

这里随机数随机生成 0~3 ,每行中只有一个会和 i 相同,这个就是黑色块,其余的就是白色块,

让场景初始化的时候调用自定义的开始函数

1
2
3
4
5
6
7
8
9
10
// HelloWorldScene.cpp

void HelloWorld::startGame()
{
addStartLine();

addNormalLine(1);
addNormalLine(2);
addNormalLine(3);
}

现在,基础的样子已经呈现出来了


逻辑,Logic

别踩白块游戏,主要逻辑是让玩家按照顺序,点击黑色的块,避免点击白色的块,所以要实践触摸事件。

首先,判断点击事件时,要遍历所有的块,所以需要获取所有块的方法。

1
2
3
4
5
6
// Block.cpp

// 获取所有的块
Vector<Block*> * Block::getBlocks(){
return Block::blocks;
}

然后,添加监听器,监听触摸事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// HelloWorldScene.cpp

bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
// 获取屏幕大小
visibleSize= Director::getInstance()->getVisibleSize();
// 开始游戏
startGame();

// 添加监听器
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan= [this](Touch* t,Event* e){

auto bs = Block::getBlocks();
Block *b;
for (auto it = bs->begin();it!=bs->end();it++)
{
b = *it;
//游戏逻辑的实现
// 如果点击
if( b->getLineIndex()==1 && b->getBoundingBox().containsPoint(t->getLocation()) )
{
if (b->getColor()==Color3B::BLACK)
{
// 触摸到黑色块,就让它变成灰色
b->setColor(Color3B::GRAY);
}
else
{
// 未触摸到该行黑色块,就提示 游戏失败
MessageBox("GameOver","失败");
}
}
}

return false;
};

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

return true;
}

现在,我们实现了第一行的逻辑,只能点黑块,不能点白块。

接下来,要在点击正确后,将所有行整体下移一行,最上面产生一个新行,我们通过新的方法来实现该逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// HelloWorldScene.cpp

// 向下移动一行
void HelloWorld::moveDown(){
// 产生新的一行
addNormalLine(4);

auto bs = Block::getBlocks();
//遍历所有方块,让方块向下移动,最下面的一行消除
for (auto it = bs->begin();it!=bs->end();it++)
{
(*it)->moveDown();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Block.cpp 

void Block::moveDown() {
Size visibleSize = Director::getInstance()->getVisibleSize();
this->lineIndex--;
//时间用0.1秒,移动到指定的位置(X方向不变,Y方向)
runAction(Sequence::create(MoveTo::create(0.1f,Point(getPositionX(),lineIndex*visibleSize.height/4)),
CallFunc::create([this](){
if (lineIndex<0)
{
this->removeBlock();
}
}),NULL));
}

接下来,处理游戏结束时的情况。

目前的玩法就是 走50行,然后计算花费的时间。

因此,需要记录已经实现的行数,然后在走到第50行的时候判断

  • 若已经显示最后一行(全绿),就无需动作
  • 若还没有显示最后一行,需要执行新动作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// HelloWorldScene.cpp

// 向下移动一行
void HelloWorld::moveDown(){

if( lineCount < 50 ) {
// 产生新的一行
addNormalLine(4);
}
else if( !isEnd ) {
addEndLine();
isEnd = true;
}

auto bs = Block::getBlocks();
//遍历所有方块,让方块向下移动,最下面的一行消除
for (auto it = bs->begin();it!=bs->end();it++)
{
(*it)->moveDown();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// HelloWorldScene.cpp

bool HelloWorld::init()
{
if ( !Layer::init() )
{
return false;
}
// 获取屏幕大小
visibleSize= Director::getInstance()->getVisibleSize();
// 开始游戏
startGame();

// 添加监听器
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan= [this](Touch* t,Event* e){

auto bs = Block::getBlocks();
Block *b;
for (auto it = bs->begin();it!=bs->end();it++)
{
b = *it;
//游戏逻辑的实现
// 如果点击
if( b->getLineIndex()==1 && b->getBoundingBox().containsPoint(t->getLocation()) )
{
if (b->getColor()==Color3B::BLACK)
{
// 触摸到黑色块,就让它变成灰色
b->setColor(Color3B::GRAY);
this->moveDown();
}
else if( b->getColor()==Color3B::GREEN)
{
this->moveDown();
}
else
{
// 未触摸到该行黑色块,就提示 游戏失败
MessageBox("GameOver","失败");
}
break;
}
}

return false;
};

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

return true;
}

现在可以开始和结束了,游戏生命周期算完整了。

最后,再处理一下计时相关。

首先是显示,块是一行行在移动的,但是时间文本是固定不变的,因此可以分为两层,一层显示文本,一层处理块。

1
2
3
4
5
6
7
8
9
10
11
12
// HelloWorldScene.cpp   init函数

// 添加游戏层
gameLayer = Node::create();
addChild(gameLayer);

// 添加时间
timerLabel = Label::create();
timerLabel ->setTextColor(Color4B::BLUE);
timerLabel->setSystemFontSize(48);
timerLabel->setPosition(visibleSize.width/2,visibleSize.height-100);
addChild(timerLabel);

然后处理时间的 开始、停止、更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void HelloWorld::startTimer()
{
if (!isTimerRunning)
{
scheduleUpdate();
//获取到了clock的时间
startTime = clock();

isTimerRunning = true;
}
}

void HelloWorld::stopTimer()
{
if (isTimerRunning)
{
unscheduleUpdate();

isTimerRunning = false;
}
}

void HelloWorld::update( float dt )
{
//时间的偏差
long offset = clock()-startTime;
//强转offset为double类型
timerLabel->setString(StringUtils::format("%g",((double)offset)/1000));
}

最后在触碰判断处,设定开始计时和停止计时的时机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// HelloWorldScene.cpp  init函数  监听器部分

// 添加监听器
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan= [this](Touch* t,Event* e){

auto bs = Block::getBlocks();
Block *b;
for (auto it = bs->begin();it!=bs->end();it++)
{
b = *it;
//游戏逻辑的实现
// 如果点击
if( b->getLineIndex()==1 && b->getBoundingBox().containsPoint(t->getLocation()) )
{
if (b->getColor()==Color3B::BLACK)
{
if (!isTimerRunning)
{
this->startTimer();
}
// 触摸到黑色块,就让它变成灰色
b->setColor(Color3B::GRAY);
this->moveDown();
}
else if( b->getColor()==Color3B::GREEN)
{
this->moveDown();
this->stopTimer();
}
else
{
// 未触摸到该行黑色块,就提示 游戏失败
MessageBox("GameOver","失败");
}
break;
}
}

return false;
};


优化,Optimization

第一点,触摸屏蔽;即每次踩到白块后,屏蔽触摸,避免后续异常行为。

通过新建屏蔽层来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SwallowLayer.h  

#include "cocos2d.h"

USING_NS_CC;

class SwallowLayer : public Layer
{
public:
/***** 初始化函数 *****/
virtual bool init();
CREATE_FUNC(SwallowLayer);

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SwallowLayer.cpp

#include "SwallowLayer.h"

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

// 添加监听器
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan= [this](Touch* t,Event* e){
CCLOG("touch swallow layer");
return true;
};
listener->setSwallowTouches(true);
Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);


return true;
}

第二点,记录最高分。

本游戏有两个模式:固定时间、固定函数。

每次游戏结束,都要向游戏结束界面传递对应数据,比如,在固定时间模式下显示花费的时间,在固定行数模式下显示完成行数。

通过对游戏结束层的函数重载来实现。

1
2
3
//接受模式(Limit Block OR Limit Time ),// true 为LimitBlocks,false为LimitTime
void createText( bool mode , double num );
void createText( bool mode );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void GameOver::createText( bool mode , double num )
{
if( mode ) {
// 获取最高分
double highest = UserDefault::getInstance()->getDoubleForKey("HIGHESTTIME",0);

CCLOG("highest time %f",highest);


auto _highest = Label::create(StringUtils::format("Highest: %.2f",highest),"fonts/Marker Felt.ttf",34);
_highest->setTextColor(Color4B::RED);
_highest->setPosition(visibleSize.width/2+100,visibleSize.height/2+100);
this->addChild(_highest);

auto _score = Label::create(StringUtils::format("Yours: %.2f",num),"fonts/Marker Felt.ttf",60);
_score->setTextColor(Color4B::BLUE);
_score->setPosition(visibleSize.width/2,visibleSize.height/2-50);
this->addChild(_score);

if( highest > num || highest == 0 ) {
CCLOG("this time %f",num);
UserDefault::getInstance()->setDoubleForKey("HIGHESTTIME",num);
CCLOG("this highest time %f",UserDefault::getInstance()->getDoubleForKey("HIGHESTTIME",num));
}
}
else
{
// 获取最高分
int highest = UserDefault::getInstance()->getIntegerForKey("HIGHESTBLOCKS",0);

auto _highest = Label::create(StringUtils::format("Highest: %d",highest),"fonts/Marker Felt.ttf",34);
_highest->setTextColor(Color4B::RED);
_highest->setPosition(visibleSize.width/2+100,visibleSize.height/2+100);
this->addChild(_highest);

auto _score = Label::create(StringUtils::format("Yours: %d",(int)num),"fonts/Marker Felt.ttf",60);
_score->setTextColor(Color4B::BLUE);
_score->setPosition(visibleSize.width/2,visibleSize.height/2-50);
this->addChild(_score);

if( highest < num ) {
UserDefault::getInstance()->setIntegerForKey("HIGHESTBLOCKS",(int)num);
}
}
}

void GameOver::createText( bool mode )
{
// mode——true:Limit Block,false:Limit Time
if( mode ) {
// 获取最高分
double highest = UserDefault::getInstance()->getDoubleForKey("HIGHESTTIME",0);

auto _highest = Label::create(StringUtils::format("Highest: %.2f",highest),"fonts/Marker Felt.ttf",34);
_highest->setTextColor(Color4B::RED);
_highest->setPosition(visibleSize.width/2+100,visibleSize.height/2+100);
this->addChild(_highest);
}
else
{
// 获取最高分
long highest = UserDefault::getInstance()->getIntegerForKey("HIGHESTBLOCKS",0);

auto _highest = Label::create(StringUtils::format("Highest: %d",highest),"fonts/Marker Felt.ttf",34);
_highest->setTextColor(Color4B::RED);
_highest->setPosition(visibleSize.width/2+100,visibleSize.height/2+100);
this->addChild(_highest);
}

auto _label = Label::create("Fail!","fonts/Marker Felt.ttf",60);
_label->setTextColor(Color4B::RED);
_label->setPosition(visibleSize.width/2,visibleSize.height/2-50);
this->addChild(_label);

}

第三点,时间显示的优化,就是做一个格式化。

1
2
3
4
5
6
7
8
9
double modeLimitTime::getMillSecond()
{
struct timeval tv;
gettimeofday(&tv, nullptr);

CCLOG("CurrentTime MillSecond %f", (double)tv.tv_sec * 1000 + (double)tv.tv_usec / 1000);

return (double)tv.tv_sec * 1000 + (double)tv.tv_usec / 1000;
}
1
2
3
4
5
//时间的偏差
_time = (getMillSecond()-startTime)/1000.0;
CCLOG("SubTime MillSecond %f", _time);
//强转offset为double类型,显示小数点后两位
timerLabel->setString(StringUtils::format("%.2f",_time));





资料