Cocos2dx项目实战(2)——别踩白块

这一次让我们尝试做一个经典的小游戏。

本文仅供个人记录和复习,不用于其他用途

游戏效果展示

简单的说一下,我们必须点击黑块,才能够往下走一步,点击白块是没有效果的。当点击到绿块时,计时就会停止,游戏就结束了。当然,我们只能够点击第二行(最下面的是第一行)。

具体思路

我们需要两个类,一个用于生成游戏的层,一个用于生成块。生成块的类需要设置块的大小、位置、颜色。那么为了方便起见,我们就设立好最基本的创建方法,然后通过参数传递来获取不同颜色、不同大小、不同样式的块。至于层的类,就需要设置游戏的启动、判断、结束等等逻辑。

这里我用的是3.10的版本,所以不会使用2.x版本的编写方法。

Block类

Block.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef __Block__H__
#define __Block__H__
#include "cocos2d.h"
USING_NS_CC;
class Block: public Sprite
{
public:
static Block *create(Size size, Color3B color, String str, Color3B strColor);
bool init(Size size, Color3B color, String str, Color3B strColor);
Size winSize = Director::getInstance()->getWinSize();
static Vector<Block *> vector;
static Vector<Block *> &getBlocksVector();
CC_SYNTHESIZE(int, _lineIndex, LineIndex);
void moveDownAndCleanUp();
};
#endif

这里我没有使用CREATE_FUNC()这个宏,因为这个宏生成的方法没有参数,而创建Block对象时需要传入参数,所以我们得自己编写创建方法。winSizevectorgetBlocksVector()我就不多说了,这里讲一下这个CC_SYNTHESIZE()

1
2
3
4
#define CC_SYNTHESIZE(varType, varName, funName)\
protected: varType varName;\
public: virtual varType get##funName(void) const { return varName; }\
public: virtual void set##funName(varType var){ varName = var; }

这个宏主要是快速生成一个变量,并且实现对应的get()方法和set()方法。例如这里,我们定义了一个protected下的int类型变量_lineIndex,这个变量主要的作用是记录Block对象的行号。然后,我们可以使用setLineIndex()改变它的行号,还可以使用getLineIndex来获取它的行号。

至于moveDownAndCleanUp(),这个就是为了方块在下移出屏幕时,将方块从vector中移除,并且在渲染树中清除掉它。

Block.cpp

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
#include "Block.h"
Vector<Block *> Block::vector;
Block *Block::create(Size size, Color3B color, String str, Color3B strColor)
{
Block *pRet = new Block();
if (pRet && pRet->init(size, color, str, strColor))
{
pRet->autorelease();
vector.pushBack(pRet);
}
else
{
delete pRet;
pRet = nullptr;
}
return pRet;
}
bool Block::init(Size size, Color3B color, String str, Color3B strColor)
{
if (!Sprite::init())
{
return false;
}
setContentSize(size);
setTextureRect(CCRectMake(0, 0, size.width, size.height));
setColor(color);
setAnchorPoint(Vec2(0, 0));
Label *label = Label::createWithSystemFont("", "Arial", 30);
label->setString(str.getCString());
label->setColor(strColor);
label->setPosition(Vec2(size.width / 2, size.height / 2));
addChild(label);
return true;
}
Vector<Block *> &Block::getBlocksVector()
{
return vector;
}
void Block::moveDownAndCleanUp()
{
_lineIndex--;
MoveTo *to = MoveTo::create(0.01, Vec2(getPositionX(), getPositionY() - winSize.height / 4));
this->runAction(to);
if (_lineIndex < 0)
{
vector.eraseObject(this);
removeFromParentAndCleanup(true);
}
}

首先,初始化静态成员变量vectorcreate()方法和我们之前所讲的基本相同,改变的只有传入了四个参数,以及在成功分配内存后将Block对象加入vector中。

init()中可以看到,我们对Block对象进行各种参数的设置。setContentSize()是设置容器大小;setTextureRect()其实就是控制显示的内容范围,使得这个对象成为一个矩形;颜色和锚点也需要我们设置一下。另外,这里我们创建了一个label,用于在方块中央显示文字。当然,如果你不需要显示就可以传入空的字符串。

getBlocksVector()就是返回vector,这里只说一说moveDownAndCleanUp()。我们在点击了黑块之后,所有的方块都会往下移动一行。所以,这里我们要编写一个移动动作,将方块向下移动1/4个屏幕的大小(也就是一行)。还有,当方块移出了屏幕时,我们就应该立马删除它。不过这里的顺序不能反,应该先从vector中删除,然后再从渲染树上移除。vector如果在后的话,删除时就会报错。

LayerGame类

LayerGame.h

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
#ifndef __LayerGame__H__
#define __LayerGame__H__
#include "cocos2d.h"
USING_NS_CC;
class LayerGame : public Layer
{
public:
static Scene *scene();
CREATE_FUNC(LayerGame);
bool init();
Size winSize = Director::getInstance()->getWinSize();
bool showEnd;
bool isRunning;
long startTime;
Label *label;
CC_SYNTHESIZE(int, _lineCount, LineCount);
void startGame();
void addStartLineBlocks();
void addEndLineBlocks();
void addNormalLineBlocks(int lineIndex);
bool onTouchBegan(Touch * touch, Event * pEvent);
void moveDown();
void startTimer();
void stopTimer();
void update(float dt);
};
#endif

showEndisRunning都是用于判断,之后会提到。startTime用于记录开始时间,_lineCount_lineIndex差不多,不过这个是用于记录游戏总共进行了多少行。

LayerGame.cpp

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include "LayerGame.h"
#include "Block.h"
Scene *LayerGame::scene()
{
Scene *scene = Scene::create();
LayerGame *layer = LayerGame::create();
scene->addChild(layer);
return scene;
}
bool LayerGame::init()
{
if (!Layer::init())
{
return false;
}
startGame();
setLineCount(0);
showEnd = false;
isRunning = false;
startTime = 0;
label = Label::createWithSystemFont("0.00", "Arial", 30);
label->setZOrder(100);
label->setPosition(Vec2(winSize.width / 2, winSize.height - 40));
label->setColor(Color3B::BLUE);
addChild(label);
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(LayerGame::onTouchBegan, this);
listener->setSwallowTouches(true);
Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(listener, 8);
return true;
}
void LayerGame::startGame()
{
addStartLineBlocks();
addNormalLineBlocks(1);
addNormalLineBlocks(2);
addNormalLineBlocks(3);
}
void LayerGame::addStartLineBlocks()
{
Size startBlockSize = Size(winSize.width, winSize.height / 4);
Block *b = Block::create(startBlockSize, Color3B::YELLOW, "Start Game", Color3B::BLACK);
b->setPosition(Vec2(0, 0));
addChild(b);
b->setLineIndex(0);
_lineCount++;
}
void LayerGame::addNormalLineBlocks(int lineIndex)
{
Size normalBlockSize = Size(winSize.width / 4 - 1, winSize.height / 4 - 1);
int index = rand() % 4;
for (int i = 0; i != 4; i++)
{
Block *b = Block::create(normalBlockSize, (i == index ? Color3B::BLACK : Color3B::WHITE), "", Color3B::WHITE);
b->setLineIndex(lineIndex);
b->setPosition(Vec2(i*winSize.width / 4, lineIndex*winSize.height / 4));
addChild(b);
}
_lineCount++;
}
void LayerGame::addEndLineBlocks()
{
Block *b = Block::create(winSize, Color3B::GREEN, "Game Over", Color3B::BLACK);
b->setAnchorPoint(Vec2(0, 0));
b->setPosition(Vec2(0, winSize.height));
addChild(b);
b->setLineIndex(4);
_lineCount++;
}
bool LayerGame::onTouchBegan(Touch * touch, Event * pEvent)
{
for (auto &b : Block::getBlocksVector())
{
if (b->boundingBox().containsPoint(touch->getLocation()) && b->getLineIndex() == 1)
{
if (b->getColor() == Color3B::BLACK)
{
b->setColor(Color3B::GRAY);
startTimer();
this->moveDown();
}
else if (b->getColor() == Color3B::WHITE)
{
}
else if (b->getColor() == Color3B::GREEN)
{
stopTimer();
this->moveDown();
}
break;
}
}
return false;
}
void LayerGame::moveDown()
{
if (getLineCount() < 10)
{
addNormalLineBlocks(4);
}
else if (!showEnd)
{
addEndLineBlocks();
showEnd = true;
}
for (auto &b = Block::getBlocksVector().rbegin(); b != Block::getBlocksVector().rend(); b++)
{
(*b)->moveDownAndCleanUp();
}
}
void LayerGame::startTimer()
{
if (!isRunning)
{
scheduleUpdate();
startTime = clock();
isRunning = true;
}
}
void LayerGame::stopTimer()
{
if (isRunning)
{
unscheduleUpdate();
}
}
void LayerGame::update(float dt)
{
long long offset = clock() - startTime;
String *str = String::createWithFormat("%g", (double)offset / 1000);
label->setString(str->getCString());
}

看看init()方法,我们调用startGame()开始游戏,并将总行数_linCount初始化为0,label是用来显示时间的。接下来,我们创建一个监听器,并且将之绑定到层上(注意,这个绑定方法只能绑定到层上,不能指定对象)。

startGame()很简单,我们先调用addStartLineBlocks(),这个其实就是生成最下面的那个黄色长条,中间有一个Start Game。然后我们生成第二、三、四行。那么至此,游戏的初始化就完成了。

addStartLineBlocks()里面有一点要注意,我们必须给这个长条标上行号0,并且总行数加1。另外,我们每执行一次addNormalLineBlocks(),我们都要将总行数加1。我们每创建一个Block对象,都要给它标注行号,不然的话之后的下移和删除操作将没法进行。addEndLineBlocks()其实就是加上最后的绿色结束块。

onTouchBegan()就是触摸事件的回调函数,这里我们用for的范围循环来遍历vector中的每一个元素。boundingBox()表示对象的范围,containsPoint()用于检测坐标有没有在对象上。那么这里呢,我们可以使用getLocation()来获取鼠标点击的位置,从而可以检测到方块有没有被点中。当然,这里我们还要加一个条件,那就是方块必须处于第二行_lineIndex = 1,也就是我们只能在第二行点击方块。

随后,如果方块是黑色,我们调用moveDown()让方块下移,并把方块改成灰色。这里我们调用startTime()开始计时。如果是绿色,我们就调用stopTimer()停止即使,并且让绿色结束块下移一行。

moveDown()中,如果总行数小于10,那么我们就继续增加第四行。如果超过了10行,那么我们就应该出现结束块,这里showEnd是为了让结束块只出现一次。当然,这里最主要的是下移操作,我们这里使用的是反向遍历。为什么要使用反向遍历?让我们先看看moveDownAndCleanUp()

之前已经说过了,moveDownAndCleanUp()先将方块从vector中删除,然后从渲染树中移除。那么,如果我们正向遍历,在vector删除一个元素之后,Cocos会将后面的元素往前移动进行填补。当迭代器移动到最后一个时,因为元素全都前移了,所以最后一个对象为空,就会发生错误。所以我们只能够反向遍历。

startTimer()开启了一个定时器,用于改变label中的内容,从而显示出游戏用时。isRunning用于确保startTimer()只被运行一次。stopTimer()用于关闭定时器,停止计时。至于时间是怎么获取的,我们可以先用startTime获取一次时间,然后不断地用clock()返回的时间减去startTime,这样就能够获得游戏用时。

总结

游戏看上去非常简单,但要实现却不是那么容易。无论是节点的删除顺序,还是遍历的方向,都有可能影响游戏的正常运行,总而言之,在制作游戏前一定要有很好的规划,不能够盲目地开始编写代码。