Cocos2d实战之消灭星星

Cocos2d-x 消灭星星


前言

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



原文

环境:

  • Cocos2d-x 3.4
  • VS2012

一、游戏场景 & 选中星星

星星类

1
2
3
4
5
6
7
8
9
10
11
12
       /*           初始化           */
Star();
static Star* create( int row , int col );

// 设定星星显示状态
void setDisplayMode(DisplayMode mode);

/* 设定函数 */
CC_SYNTHESIZE(int, m_row, Row);
CC_SYNTHESIZE(int, m_col, Col);
CC_SYNTHESIZE(int, m_imgIndex, ImgIndex);
CC_SYNTHESIZE_READONLY(DisplayMode, m_displayMode, DisplayMode);

GameDefine文件,存储游戏内的一些常量设定

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
// 定义屏幕宽高,这与所做的图片有关
#define GAME_SCREEN_WIDTH 480
#define GAME_SCREEN_HEIGHT 800

// 定义每个精灵大小与边框大小
#define STAR_WIDTH 48
#define BOADER_WIDTH 0.5

// 行列值
#define ROWS 10
#define COLS 10

// 关于这些星星
// 星星的模式,NORMAL 普通状态,HEART 高亮状态
enum DisplayMode{
DISPLAY_MODE_NORMAL = 0,
DISPLAY_MODE_HEART
};
// 星星总数
#define TOTAL_STAR 5
// 普通状态
static const char *starNormal[TOTAL_STAR] = {
"red.png",
"blue.png",
"green.png",
"purple.png",
"yellow.png"
};

// 高亮状态
static const char *starHeart[TOTAL_STAR] = {
"red_heart.png",
"blue_heart.png",
"green_heart.png",
"purple_heart.png",
"yellow_heart.png"
};

游戏场景,用一个二维数组来存储星星个体,通过plist文件加载图集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 加载plist和png
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("stars.plist");
starSheet = Sprite::create();
starSheet->setAnchorPoint(Vec2::ZERO);
this->addChild(starSheet,1);
// 初始化坐标值
mapLBX = (GAME_SCREEN_WIDTH - STAR_WIDTH * COLS - (COLS - 1) * BOADER_WIDTH) / 2;
mapLBY = (GAME_SCREEN_HEIGHT - STAR_WIDTH * ROWS - (ROWS - 1) * BOADER_WIDTH) / 2;

auto sprite = Sprite::create("bg_mainscene.jpg");
sprite->setPosition(Vec2(GAME_SCREEN_WIDTH/2,GAME_SCREEN_HEIGHT/2));
this->addChild(sprite, 0);

initMap();

初始化地图方法 & 创建星星方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化地图
void GameScene::initMap( ) {

for( int r = 0 ; r < ROWS ; ++r ){
for( int c = 0 ; c < COLS ; ++c ){
createStar(r,c);
}
}
}

// 创建星星
void GameScene::createStar( int row , int col )
{
Star* spr = Star::create(row, col);

Point pos = positionOfItem(row, col);
spr->setPosition(pos);
starSheet -> addChild(spr);

// 填充数组相应位置
map[row][col] = spr;
}

执行一下,场景显示完毕

接下来,处理选中星星,先是触摸事件的添加

1
2
3
4
5
6
// GameScene.cpp  init函数

// 触摸事件处理
auto touchListener = EventListenerTouchOneByOne::create();
touchListener->onTouchBegan = CC_CALLBACK_2(GameScene::onTouchBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

当触摸的时候,逻辑如下:当选中一个星星时,要让它和四个方向上同色的星星都处于高亮状态。

可以使用一个搜索算法实现

  1. 让当前星星处于高亮状态
  2. 将当前星星分别与和四个方向上星星进行颜色比较
    • 若相同,将该星星坐标扔进函数
    • 若不同,则跳过

当然,要记得设置标记数组,避免遍历过的星星再次遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 触摸事件
bool GameScene::onTouchBegan(Touch *touch, Event *unused) {

auto location = touch->getLocation();
Star* sta;
sta = starOfPoint(&location);
if( sta ) {
// 初始化记忆地图mapR+恢复普通状态
for( int i = 0 ; i < ROWS ; i++ ) {
for( int j = 0 ; j <COLS ; j++ ) {
mapR[i][j] = false;
map[i][j]->setDisplayMode(DISPLAY_MODE_NORMAL);
}
}
// 如果只有当前一个星星这个颜色,不要让它处于高亮状态
if( waitPop(sta->getRow(),sta->getCol()) == 1 )
sta->setDisplayMode(DISPLAY_MODE_NORMAL);
}

return true;
}

下面mapR数组就是标记数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int GameScene::waitPop(int row , int col)
{
int i,r,c,tol;
// 四个方向
int search[4][2]={-1,0,1,0,0,1,0,-1};

tol = 1;
map[row][col]->setDisplayMode(DISPLAY_MODE_HEART);
mapR[row][col] = true;

// 四个方向遍历
for( i = 0 ; i < 4 ; i++ ) {
r = row + search[i][0];
c = col + search[i][1];
if( r >= 0 && r < ROWS && c >= 0 && c < COLS ) {
if( map[row][col]->getImgIndex() == map[r][c]->getImgIndex() && !mapR[r][c] ) {
mapR[r][c] = true;
tol += waitPop(r,c);
}
}
}

return tol;
}

提取设置一些工具函数,方便处理

1
2
3
4
5
6
7
// 根据行列,获取坐标值
Point GameScene::positionOfItem(int row , int col)
{
float x = mapLBX + (STAR_WIDTH + BOADER_WIDTH) * col + STAR_WIDTH / 2;
float y = mapLBY + (STAR_WIDTH + BOADER_WIDTH) * row + STAR_WIDTH / 2;
return Point(x, y);
}
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
// 根据触摸点,判断地图上的星星位置
Star* GameScene::starOfPoint(Point *point)
{
Star *sta = NULL;
Rect rect = Rect(0, 0, 0, 0);
Size sz;
sz.height=STAR_WIDTH;
sz.width=STAR_WIDTH;

for( int r = 0 ; r < ROWS ; ++r ) {
for( int c = 0 ; c < COLS ; ++c ) {
sta = map[r][c];
if( sta ) {
rect.origin.x = sta->getPositionX() - ( STAR_WIDTH / 2);
rect.origin.y = sta->getPositionY() - ( STAR_WIDTH / 2);

rect.size = sz;
if (rect.containsPoint(*point)) {
return sta;
}
}
}
}

return NULL;
}

效果如下


二、消除逻辑

处于高亮状态下的星星,再次被点击时的消除逻辑

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
// 触摸事件
bool GameScene::onTouchBegan(Touch *touch, Event *unused) {

auto location = touch->getLocation();
Star* sta;
sta = starOfPoint(&location);

// 当前对象不为空
if( sta ) {
// 看该 星星 处于什么状态
if( sta->getDisplayMode() == DISPLAY_MODE_NORMAL ) {

// 初始化记忆地图mapR+恢复普通状态
for( int i = 0 ; i < ROWS ; i++ ) {
for( int j = 0 ; j <COLS ; j++ ) {
mapR[i][j] = false;
if( map[i][j]!=NULL )
map[i][j]->setDisplayMode(DISPLAY_MODE_NORMAL);
}
}

// 如果只有当前一个星星这个颜色,不要让它处于高亮状态
if( waitPop(sta->getRow(),sta->getCol()) == 1 ) {
mapR[sta->getRow()][sta->getCol()]=false;
sta->setDisplayMode(DISPLAY_MODE_NORMAL);
}
}
else
{
deleteStar();
}

}

return true;
}

在所点击的星星非空时,判断星星是否为高亮状态,若不是,则初始化标记数组,将所有星星显示状态恢复为普通。

注意,此处所有星星的显示状态中,要把消除掉的过滤掉。

然后,是消除星星的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void GameScene::deleteStar( void )
{
int r,c;
for( r = 0 ; r < ROWS ; r++ ) {
for( c = 0 ;c < COLS ; c++ ) {
if( mapR[r][c] == true ) {
// popStar(map[i][j]);

map[r][c]->removeFromParent();
map[r][c]=NULL;
}
}
}

}

初始化标记数组,可用memset函数

1
2
3
4
5
6
7
8
GameScene::GameScene()	
: starSheet(NULL)
,mapLBX ( (GAME_SCREEN_WIDTH - STAR_WIDTH * COLS - (COLS - 1) * BOADER_WIDTH) / 2 )
,mapLBY ( (GAME_SCREEN_HEIGHT - STAR_WIDTH * ROWS - (ROWS - 1) * BOADER_WIDTH) / 2 )
{
// 初始化 mapR 数组(string.h 头文件)
memset(mapR,false,sizeof(mapR));
}

看一下消除后的效果(本来想用GIF,但是工具出了点问题,简单看个图片吧)

星星已经消除成功,接下来是消除后的整合处理。

思路:

  1. 向下归拢,按列为单位,每列中从下往上遍历
  2. 遍历到第一个为空对象,将该对象上方第一个不为空对象,落下来,不断向上遍历到最后
  3. 向左归拢,依旧按列为单位,先判断每列第一行是否为空
  4. 遍历到第一个为空对象,将该对象右方第一个不为空对象,移过来,不断向右遍历到最后
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
void GameScene::adjustStar( )
{
int r,c;
// 向下归拢
for( c = 0 ; c < COLS ; c++ ) {
for( r = 0 ; r < ROWS-1 ; r++ ) {
if( map[r][c] == NULL ) {
for( int newR = r+1 ; newR < ROWS ; newR++ ) {
Star* sta = map[newR][c];
if( sta != NULL ) {

map[r][c] = sta;
map[newR][c] = NULL;

Point startPosition = sta->getPosition();
Point endPosition = positionOfItem(r, c);
float speed = (startPosition.y - endPosition.y) / GAME_SCREEN_HEIGHT*2;
sta->stopAllActions();
sta->runAction(MoveTo::create(speed, endPosition));
sta->setRow(r);

break;
}
}
}
}
}

// 向左归拢
for( c = 0 ; c < COLS-1 ; c++ )
if( map[0][c] == NULL ) {
for( int newC=c+1; newC < COLS ; newC++ ) {
if( map[0][newC] != NULL ) {
for( int moveR = 0 ; moveR < ROWS ; moveR++ ) {
Star* sta = map[moveR][newC];

if( sta!=NULL ) {
map[moveR][c] = sta;
map[moveR][newC] = NULL;

Point startPosition = sta->getPosition();
Point endPosition = positionOfItem(moveR,c);
float speed = (startPosition.x - endPosition.x) / GAME_SCREEN_WIDTH*2;
sta->stopAllActions();
sta->runAction(MoveTo::create(speed, endPosition));
sta->setCol(c);
}
}
break;
}
}
}
}

效果展示

最后,判断游戏是否结束

思路:

  • 由于数组不大,直接遍历即可
  • 将本对象与上下左右四个方向上对象比较
    • 如果有同颜色,直接返回false(表示 未结束)
    • 如果无同颜色,返回true(表示 结束)
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
bool GameScene::isFinish( )
{
int i,r,c;
// 四个方向
int search[4][2]={-1,0,1,0,0,1,0,-1};

for( r = 0 ; r < ROWS ; r++ ) {
for( c = 0 ; c < COLS ; c++ ) {
if( !map[r][c] ) continue;

for( i = 0 ; i < 4 ; i++ ) {
int nr = r + search[i][0];
int nc = c + search[i][1];

if( nr>=0 && nr<ROWS && nc>=0 && nc<COLS ) {
if( map[nr][nc]!=NULL ) {
if( map[r][c]->getImgIndex() == map[nr][nc]->getImgIndex() )
return false;
}
}
}
}
}

return true;
}

最后效果


三、特效

爆炸特效

在星星消除后,加一些爆炸的粒子特效

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
void starSpecialEffect(Star* sta,Point position,Node* node,int staNum)
{
// 创建爆炸粒子效果,粒子数量为 staNum 个
ParticleExplosion* effect = ParticleExplosion::createWithTotalParticles(staNum);
// 设置此粒子特效的纹理图片
effect->setTexture(Director::getInstance()->getTextureCache()->addImage("star.png"));
// 设置开始时候的粒子颜色
effect->setStartColor(getColor4F(sta->getImgIndex()));
effect->setStartColorVar(Color4F(0,0,0,1));
effect->setEndColor(getColor4F(sta->getImgIndex()));
effect->setEndColorVar(Color4F(0,0,0,1));
// 设置开始时的粒子大小
effect->setStartSize(20.0f);
effect->setGravity(Point(0,-300));
// 设置粒子生命周期
effect->setLife(2.0f);
// 设置粒子速度
effect->setSpeed(200);
effect->setSpeedVar(10);
// 设置粒子位置
effect->setPosition(position);
node->addChild(effect,2);
}

Color4F getColor4F(int imgIndex)
{
switch(imgIndex){
case 0:
//red
return Color4F::RED;
case 1:
//blue
return Color4F::BLUE;
case 2:
//green
return Color4F::GREEN;
case 3:
//purple
return Color4F(128.0f,0,128.0f,1.0f);
case 4:
//yellow
return Color4F::YELLOW;
}
return Color4F(1,1,1,0);
}

效果如下

音乐音效

此处用UserDefault存储开关,此处并没有分开处理,而是一个开关控制。

背景音乐,需要重载虚函数

1
2
3
4
// 场景加载完成后进行
virtual void onEnterTransitionDidFinish();
// 场景被消除时执行
virtual void cleanup();

添加方法

1
2
3
4
5
6
7
8
9
10
11
12
void WelcomeScene::onEnterTransitionDidFinish()
{
Layer::onEnterTransitionDidFinish();
if ( userDefault->getBoolForKey("MusicKey") ) {
SimpleAudioEngine::getInstance()->playBackgroundMusic("Music/music.ogg", true);
}
}
void WelcomeScene::cleanup()
{
Layer::cleanup();
SimpleAudioEngine::getInstance()->stopBackgroundMusic();
}

漂浮文字

1
2
3
4
5
6
7
8
9
10
FloatWord* FloatWord::create( const std::string& word,const int fontSize,Vec2 begin )
{
FloatWord* fw = new FloatWord();
if( !fw->init(word,fontSize,begin) ) {
return NULL;
}

fw->autorelease();
return fw;
}
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
void FloatWord::floatInOut(const float speed,const float delayTime,std::function<void()> callback){

MoveTo* moveIn = MoveTo::create(speed,Vec2(GAME_SCREEN_WIDTH/2,fw_begin.y));
MoveTo* moveOut = MoveTo::create(speed,Vec2(-fw_label->getContentSize().width,fw_begin.y));

CallFunc* call = CallFunc::create(callback);

Sequence* action = Sequence::create(moveIn,DelayTime::create(delayTime),moveOut,call,NULL);
fw_label->runAction(action);
}

void FloatWord::floatIn(const float speed){

MoveTo* moveIn = MoveTo::create(speed,Vec2(GAME_SCREEN_WIDTH/2,fw_begin.y));

Sequence* action = Sequence::create(moveIn,NULL);
fw_label->runAction(action);
}

void FloatWord::floatOut(const float speed,const float delayTime){

MoveTo* moveOut = MoveTo::create(speed,Vec2(-fw_label->getContentSize().width,fw_begin.y));

Sequence* action = Sequence::create(DelayTime::create(delayTime),moveOut,NULL);
fw_label->runAction(action);
}

添加后效果:

Combo特效 & 最终星星消除

首先是combo特效,每次消除的时候,判断消除的个数是否满足combo条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void comboEffect(int num,Node* node){

if( num < 5 )
return;

Sprite* comboSprite;
if( num >= 10 ){
comboSprite = Sprite::create("combo_3.png");
}else if( num >= 7 ){
comboSprite = Sprite::create("combo_2.png");
}else{
comboSprite = Sprite::create("combo_1.png");
}

comboSprite->setPosition(Vec2(GAME_SCREEN_WIDTH/2,GAME_SCREEN_HEIGHT/2));
node->addChild(comboSprite,4);

Blink* blink = Blink::create(1.0f,5);
CallFunc* remove = CallFunc::create([=](){comboSprite->removeFromParentAndCleanup(true);});
Sequence* action = Sequence::create(blink,remove,nullptr);
comboSprite->runAction(action);
}

最后星星消除,流程如下

  1. 判断此关卡结束(没有可以消除的星星),然后将全局的关卡结束变量设置为true,获得剩余星星的数量,如果剩余星星数量大于10则设置为10个(因为一个个消除的效果,最多10个)
  2. 在update函数中(每一帧都会调用),会判断关卡是否结束;若结束,调用消除函数,10个以内,每消除一个都会返回,不会继续消除,而且记录消除时间,消除一次后一定时间间隔再进行下一次消除
  3. 消除完10个(或者小于10个)以后,不会再返回,会一次性将剩余星星消除完毕,将关卡结束变量设置为false
1
2
3
4
5
6
7
8
9
10
11
12
if( isFinish() )	{
isLevelFinish = true;
int temp = totalStarNum();

if( temp <= 10 ) {
needDelOneByOne = temp;
}
}
else
{
needDelOneByOne = 10;
}

update函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void GameScene::update( float dt )
{
// 分数变化
Label *labelScore = (Label *)this -> getChildByTag(6);
labelScore -> setString( StringUtils::format("Score: %d ",_score));

// 如果当前关卡结束 星星一个个消除的实现
if( isLevelFinish ) {

deleteTime += dt;
if( deleteTime > DELSTAR_ONEBYONE_TIME ) {
popFinishStar(needDelOneByOne);
needDelOneByOne--;
deleteTime = 0;
}
}
}

消除最终星星函数

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
void GameScene::popFinishStar( int n )
{
int r,c;
Star* sta;

for( r = ROWS-1 ; r >= 0 ; r-- ) {
for( c = 0 ; c < COLS ; c++ ) {
sta = map[r][c];
if( sta ) {
starSpecialEffect(sta,sta->getPosition(),this,5);
map[r][c]=NULL;
sta->removeFromParentAndCleanup(true);
return;
}
else
{
starSpecialEffect(sta,sta->getPosition(),this,totalStarNum()*4);
map[r][c]=NULL;
sta->removeFromParentAndCleanup(true);
}
}
}
}


isLevelFinish = false;
scheduleOnce(schedule_selector(GameScene::levelOver),2.0f);
}

各种combo效果

最高分记录

依旧用UserDefault存储

1
2
if( userDefault->getIntegerForKey("HightScore") < _score )
userDefault->setIntegerForKey("HightScore",_score);

场景的存储用了push和pop,

在主界面定义一个变量,来判断是否可以继续,

刚进入主界面时,场景栈是没有场景存储的,此时点击继续游戏就会退出游戏,所以要在场景栈没有场景时,不允许点击继续游戏按钮,

这个变量,在从游戏界面跳转到主界面(通过返回函数跳转)时,会设置为true,就是可以点击。

消除星星Hint

当消除几个星星,都要有提示,加了多少分。

游戏结束,如果剩余星星数小于10个,还会有额外加分。

星星消除逻辑

  • 一个等差数列,第一个星星5分,第二个15分,第三个25分(首项为5,公差为10)

  • 当消除n个星星

    1
    n*5 + n*(n-1) * 10 / 2

剩余星星数量,所获得的额外分数,则通过数组,放在GameDefine头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 剩余星星所奖励的分数
static const int rewardScore[11] = {
2000,
1980,
1920,
1820,
1680,
1500,
1280,
1020,
720,
380,
0
};




资料