Cocos2dx项目实战(6)——2048小游戏

这一章将介绍一款特别经典的小游戏,同时也会教大家如何将游戏打包成apk文件

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

前言

之前的项目实战贴出了所有的源码,而一些游戏的重要部分讲的不是很清楚,代码太多看起来也头晕。所以呢,这一章开始我只会解析一些重要的部分,源码我已开源至GitHub

游戏设计

看看下面的游戏界面,其实基本的元素很好判断,先自己想想一下要做什么,再接着往下看。

想好了吗?主要分为几个要素:

  • 总分数记录和变动
  • 卡片的数字和位置变动
  • 判断游戏结束
  • 重来和退出

其实第一和第四条我们在之前已经做过类似的,实现起来很容易,最主要的还是卡片和游戏结束的判断。游戏的主界面我做成了一张大图:

我们只需要设计一个卡片类,然后生成16张卡片放到对应的位置即可。当然,你可能会问,卡片的合并操作怎么办。其实我们只需要按照一下的规则即可:

  • 朝一个方向滑动时,若前方为空,那么将所有卡片全部顶格
  • 若相邻的卡片数字相同,那么将它们合并
  • 每张卡片只能合并一次

注意,假如有四张卡片都是4,当它们合并时,只会合并出两张8,而不是一张16。

具体实现

游戏其实不难,之前我就用C语言实现了一遍,虽然命令行看起来很是蛋疼,不过确实可以玩。下面将就游戏主要的代码进行解析。

卡片类

卡片类只需要做到创建、修改分数、获取分数即可。当然,我们也需要根据对应的数字来修改背景颜色和字体大小。其实最开始我是直接将分数和卡片合在一起,这样就不需要自己再加数字上去,方便很多。但是呢,如果这么做那么游戏的可玩性就大打折扣,玩到2048就只能够判定游戏结束了。为了游戏能够继续玩下去,我只做了卡片的背景色,数字就是根据分数添加上去,这样就能够玩出几万分这种吧。

这里我们有一个setNum()的方法,根据传入的分数,来修改精灵的图片,并且修改数字。要注意的是,2和4是棕色的,其他的数字都是白色的,这个主要是因为背景色的缘故才这么做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 2和4需要改颜色,8到64不需要处理,128到512要把字体变小,1024到8192要变得更小,至于10000以上就更小一点。每个部分都要改颜色。
switch (number)
{
case 2:
case 4: scoreLabel->setScale(1); scoreLabel->setColor(Color3B(119, 109, 99)); break;
case 8:
case 16:
case 32:
case 64: scoreLabel->setScale(1); scoreLabel->setColor(Color3B(255, 255, 255)); break;
case 128:
case 256:
case 512: scoreLabel->setScale(0.9); scoreLabel->setColor(Color3B(255, 255, 255)); break;
case 1024:
case 2048:
case 4096:
case 8192: scoreLabel->setScale(0.8); scoreLabel->setColor(Color3B(255, 255, 255)); break;
default:
scoreLabel->setScale(0.6); scoreLabel->setColor(Color3B(255, 255, 255)); break;
}

我们用一个switch语句,就能轻松达到这种效果。当然你要是看不懂的话,那就得捧起C系列的语法书好好回顾一下。这里要注意的是,最好每一个部分都要修改一下颜色,我之前只在4和64中修改了颜色,这样就会出现当卡片变为128时,颜色还是棕色。

至于数字添加很简单,一个标签就可以做到,不过要记得把位置设在中心:

1
2
scoreLabel->setAnchorPoint(Vec2(0.5, 0.5));
scoreLabel->setPosition(Vec2(44, 44));

我这里还添加了动画效果,卡片出现和消失时会有变大和变小的动画,看起来更加生动一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
void CardSprite::show()
{
// 从小到大
auto action = Sequence::createWithTwoActions(ScaleTo::create(0, 0), ScaleTo::create(0.2f, 1));
this->runAction(action);
}
void CardSprite::hide()
{
// 从大到小
auto action = ScaleTo::create(0.2f, 0);
this->runAction(action);
}

这里只需要注意一点,当卡片数字为0时,调用hide()将其隐藏即可,数字为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
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
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(GameLayer::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(GameLayer::onTouchEnded, this);
Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(listener, this);
bool GameLayer::onTouchBegan(Touch *touch, Event *event)
{
// 记录触摸开始时的坐标
this->x = touch->getLocation().x;
this->y = touch->getLocation().y;
return true;
}
void GameLayer::onTouchMoved(Touch *touch, Event *event)
{
// 判断触摸事件是否生效,若已经生效,那么等待触摸完成
if (!isLock)
{
// 获取触摸点位移
float dx = touch->getLocation().x - this->x;
float dy = touch->getLocation().y - this->y;
// 滑动距离必须大于TOUCH_LONG,但是垂直方向的偏差不能超过TOUCH_SHORT
if (dx > TOUCH_LONG && fabs(dy) < TOUCH_SHORT)
{
// 向右,同时给触摸事件上锁,防止二次触摸
doRight();
this->isLock = true;
}
else if (dx < -TOUCH_LONG && fabs(dy) < TOUCH_SHORT)
{
// 向左
doLeft();
this->isLock = true;
}
else if (dy > TOUCH_LONG && fabs(dx) < TOUCH_SHORT)
{
// 向上
doUp();
this->isLock = true;
}
else if (dy < -TOUCH_LONG && fabs(dx) < TOUCH_SHORT)
{
// 向下
doDown();
this->isLock = true;
}
}
}
void GameLayer::onTouchEnded(Touch *touch, Event *event)
{
// 触摸结束后解锁
this->isLock = false;
}

有人可能会问,isLock是什么意思。要知道,玩家向一个方向进行滑动,不一定就是滑动了规定距离后停下来(这里TOUCH_LONG我设置的是30,你可以设小点,因为有人吐槽我做的这个游戏触屏不够灵敏)。那么,假如我们没有isLock进行锁定,而玩家拖动距离过长,那么很有可能会触发两次以上的触摸事件,这很明显不是我们想看到的。

分数合并操作

这里我就拿doLeft()进行举例:

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
void GameLayer::doLeft()
{
// 判断棋盘是否变化
bool flag = false;
bool change = false;
for (int i = 0; i != 4; i++)
{
for (int j = 0; j != 4; j++)
{
// 将一列数暂存到中间数组
temp[j] = cardArr[i][j]->getNum();
}
temp[4] = 0;
// 判断棋盘是否变化
flag = addTemp();
if (flag == true)
{
change = true;
// 改变数值
for (int j = 0; j != 4; j++)
{
cardArr[i][j]->setNum(temp[j]);
}
}
}
// 如果棋盘改变,随机位置生成一个新数
if (change == true)
{
randomInit();
}
}

我用了一个中间数组temp来存储一行(一次合并一行),既然是向左合并,那么就按照从左到右的顺序将cardArr存入temp。我们这里有一个很重要的标志,那就是棋盘是否发生了改变。如果棋盘发生了改变,那么很显然,我们就要根据temp来改变cardArr中的数字。其实大家很容易就能看出来,合并操作是在addTemp()中,doLeft()只是做一个修改卡片的作用。randomInit()就是随机在一个空位置生成2或4。

说完了doLeft(),我再来解释一下棋盘改变这个问题。比如你向左使劲划,一直划到棋盘没法滑动了,那么这个时候游戏应该不做任何反应,所以这个判断是很有必要的。

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
bool GameLayer::addTemp()
{
// 用于处理中间数组,实现一行或一列的合并操作,返回一个布尔值,为true代表棋盘改变
int n = 4;
bool change = false;
// 确保非0元素全部移向最前端
while (n--)
{
for (int i = 0; i != 4; i++)
{
if (temp[i] == 0)
{
if (temp[i] != temp[i + 1])
{
// 如果0后面的数不为0,那么后面的数要往前移动,棋盘被改变
change = true;
}
// 前移操作
temp[i] = temp[i + 1];
temp[i + 1] = 0;
}
}
}
// 判断相邻两个数是否相同,相同那么前一个数翻倍,后一个数为0,但位置暂时不改变
for (int i = 1; i != 4; i++)
{
// 判断相邻当前数和前一个数是否相等
if (temp[i] == temp[i - 1])
{
// 不等于0才合并,合并操作导致棋盘改变
if (temp[i] != 0)
{
change = true;
// 改变分数
score += temp[i] * 2;
updateScore();
}
// 前一个数翻倍,后一个数为0
temp[i - 1] *= 2;
temp[i] = 0;
}
}
n = 4;
// 将处理好的数全部往前移动
while (n--)
{
for (int i = 0; i != 4; i++)
{
if (temp[i] == 0)
{
// 若0后面的数不为0,那么把后面的数前移,同时棋盘发生改变
if (temp[i] != temp[i + 1])
{
change = true;
}
temp[i] = temp[i + 1];
temp[i + 1] = 0;
}
}
}
return change;
}

就像我之前说的,先把所有卡片全部顶格,然后相邻数字合并,最后把合并好的数字再顶格,这样就完成了一次移动操作。这里卡片移动以及数字合并都算作对棋盘的改变。

检测游戏结束

我用了一个帧循环来检测游戏是否结束,当然你也可以加在doLeft()、doRight()等等操作之中。

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
bool GameLayer::checkGame()
{
// 该方法主要用于判断棋盘是否可以移动,不能移动代表游戏结束
bool flag = false;
// 双重循环,判断有没有相同且相邻的点
for (int j = 0; j != 4; j++)
{
if (flag == true)
{
break;
}
for (int i = 0; i != 4; i++)
{
if (cardArr[i][j]->getNum() == 0 ||
i > 0 && cardArr[i][j]->getNum() == cardArr[i - 1][j]->getNum() ||
i < 3 && cardArr[i][j]->getNum() == cardArr[i + 1][j]->getNum() ||
j > 0 && cardArr[i][j]->getNum() == cardArr[i][j - 1]->getNum() ||
j < 3 && cardArr[i][j]->getNum() == cardArr[i][j + 1]->getNum())
{
flag = true;
break;
}
}
}
return flag;
}

判断逻辑非常简单,只要有空位置,或者相邻且相等的数字,那么游戏就可以进行下去,否则游戏就结束。如果返回为false,那么我们就调用gameOver(),显示游戏结束画面(因为我比较懒,就只加了几个标签,没有额外做一个层)。

总结

一路看下来,游戏真的不难,就凭我大一上学期学的C语言就能做出来(而且我学的比较差2333),不过真正要实现一个游戏还是没有那么容易的。还是那句话,我们在做游戏之前要做好设计,不要盲目动手,不然你敲了半天代码会发现毫无用处。

游戏移植安卓平台

其实移植安卓平台很简单,首先我们要下载NDK、SDK、ANT,下载下来之后解压即可。为了方便起见,我们把文件夹名就改成它们的名字。随后我们启动Cocos2dx目录下的setup.py(需要安装python2.7),也就是在目录下按住shift+右键打开命令行,然后执行python setup.py。然后我们按照所给提示,依次输入NDK、SDK、ANT的文件路径,但是这里ANT需要定位到它里面的bin文件夹,其他两个直接就是根目录即可。

设置完毕后,我们来到游戏项目下,打开proj.android,找到jni下的Android.mk。打开之后,修改里面的信息,这个是提供用户自定义的文件,不过我们只需要提供cpp文件即可。

退回到项目根目录,我们在此打开命令行,执行cocos run -p android,然后等待完成,我们就可以在bin下面找到打包好的apk文件。如果没有成功,我们就需要仔细查看信息,根据对应的信息修改错误。