Cocos2dx项目实战(8)——万圣节小猫

这次将模仿谷歌浏览器上的一个精品小游戏。

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

前言

由于这次的小游戏没那么简单,所以我会边做边写,顺便记录一些错误问题。同样,最终完成的项目源码会上传到GitHub上。

首先看一下这个游戏:https://www.google.com/doodles/halloween-2016

如果看不了的话,就直接看看下面这张图:

主场景设计

游戏的场景很简单,分为三个要素:背景、小猫、幽灵。背景部分主要是背景图和几个会动的小物品,实现起来很简单。而由于幽灵部分其实和小猫类似,所以我们第一步就是要编写游戏的主角:小猫。

身为主角,最主要的任务就是画符号消灭幽灵,而这也需要执行不同的动作,同时进行动作之间的组合与切换。

动作设计

炸毛动作

先来做个简单的动作,也就是小猫遇到幽灵会做出炸毛的表情。

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
// 炸毛动作
Sequence *Cat::getActionScare()
{
// 炸毛动作
Animation *animation1 = Animation::create();
for (int i = 1; i != 3; i++)
{
char str[50];
sprintf(str, "level1/cat-scared-%d.png", i);
animation1->addSpriteFrameWithFileName(str);
}
animation1->setDelayPerUnit(1.0 / 10.0);
Animate *animate1 = Animate::create(animation1);
// 恢复原状
Animation *animation2 = Animation::create();
animation2->addSpriteFrameWithFileName("level1/cat-scared-3.png");
animation2->addSpriteFrameWithFileName("level1/cat-stand-right-0.png");
animation2->setDelayPerUnit(1.0 / 10.0);
Animate *animate2 = Animate::create(animation2);
// 炸毛延长一秒+组合动作
DelayTime *delay = DelayTime::create(1.0);
Sequence *scareSequence = Sequence::create(animate1, delay, animate2, NULL);
return scareSequence;
}

为了做出比较好的效果,这里特意用了Sequence来拆分炸毛动作,将小猫的炸毛状态延长一秒。其实不拆分的我也试过,只是动作太快,效果并不是很好。

摆头

接下来就是编写一个比较重要的摆头动作,不仅帧数多,而且还有上下左右四个方向的摆动,当然做出来效果也很棒。

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
// 摆头动作
RepeatForever *Cat::getActionStand()
{
// 向右看,脑袋向上摆
Animation *animation1 = Animation::create();
for (int i = 0; i != 4; i++)
{
char str[50];
sprintf(str, "level1/cat-stand-right-%d.png", i);
animation1->addSpriteFrameWithFileName(str);
}
animation1->setDelayPerUnit(1.0 / 9.0);
Animate *animate1 = Animate::create(animation1);
// 向右看,脑袋向下摆
Animate *animate2 = animate1->reverse();
// 向左看,脑袋向上摆
Animation *animation3 = Animation::create();
animation3->addSpriteFrameWithFileName("level1/cat-stand-leftToRight.png");
for (int i = 0; i != 4; i++)
{
char str[50];
sprintf(str, "level1/cat-stand-left-%d.png", i);
animation3->addSpriteFrameWithFileName(str);
}
animation3->setDelayPerUnit(1.0 / 9.0);
Animate *animate3 = Animate::create(animation3);
// 向左看,脑袋向下摆
Animation *animation4 = Animation::create();
for (int i = 3; i != 0; i--)
{
char str[50];
sprintf(str, "level1/cat-stand-left-%d.png", i);
animation4->addSpriteFrameWithFileName(str);
}
animation4->addSpriteFrameWithFileName("level1/cat-stand-leftToRight.png");
animation4->addSpriteFrameWithFileName("level1/cat-stand-right-0.png");
animation4->setDelayPerUnit(1.0 / 9.0);
Animate *animate4 = Animate::create(animation4);
DelayTime *delay = DelayTime::create(0.5);
// 永久的组合动作
Sequence *seq = Sequence::create(animate1, animate2, animate3, animate4, delay, NULL);
RepeatForever *standForever = RepeatForever::create(seq);
CCLOG("done");
return standForever;
}

效果还是很不错的,代码虽然看起来长,但实际就是把摆头动作拆分为四步,并且加入到序列动作中执行即可。

动作组合

炸毛和摆头动作都做完了,实现起来是不是很容易?那么,不妨思考一个问题:如何让小猫在炸毛之后继续做摆头动作?

一般来说,多个动作同时执行,我们可以使用Spawn,而多个动作先后执行,我们可以使用Sequence。这里显然也是用Sequence

1
2
3
4
5
6
7
8
9
10
11
12
bool Level1Layer::onTouchBegan(Touch *touch, Event *event)
{
// 先停止站立的动作
_cat->stopAllActions();
Sequence *seq = Sequence::create(_cat->getActionScare(), _cat->getActionStand(), NULL);
// 执行炸毛动作
_cat->runAction(seq);
return true;
}

是不是想的一样呢?然而问题来了,当我们真正执行这段代码之后,却会出现另一种情况:小猫并不会执行摆头动作,而是在炸毛之后处于静止状态。无论再怎么点击,小猫也只会做炸毛动作。

为了解决这个问题,可以先将小猫的动作进行分解:摆头——点击屏幕后炸毛——摆头

看下这段代码:

1
2
// seq将先后执行炸毛和摆头动作
Sequence *seq = Sequence::create(_cat->getActionScare(), _cat->getActionStand(), NULL);

游戏在刚开始时,小猫便执行了摆头动作,这说明动作本身没有问题。另外,我也尝试过炸毛和摆头同时执行,虽然有点鬼畜,但依旧还是正常执行。排除这两种可能后,显然就是说明了摆头动作和序列动作存在冲突。

查看Sequence的源码:

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
Sequence* Sequence::createWithVariableList(FiniteTimeAction *action1, va_list args)
{
FiniteTimeAction *now;
FiniteTimeAction *prev = action1;
bool bOneAction = true;
while (action1)
{
now = va_arg(args, FiniteTimeAction*);
if (now)
{
prev = createWithTwoActions(prev, now);
bOneAction = false;
}
else
{
// If only one action is added to Sequence, make up a Sequence by adding a simplest finite time action.
if (bOneAction)
{
prev = createWithTwoActions(prev, ExtraAction::create());
}
break;
}
}
return ((Sequence*)prev);
}

这里其实是个递归,如果有action1、action2、action3三个动作时,首先会将前两个动作合成,然后再与第三个动作合成。递归的效率虽不高,但是代码足够的简洁。另外,Spawn也是调用了Sequence,将两个动作合并为同一个,所以同样会与摆头动作冲突。

在创建动作时,会有一个变量duration来记录动画的长度。比如动画一长为0.5s,动画二长为1s,那么duration为便1.5s。那么摆头动作呢?回头再去看时,会发现摆头动作是个RepeatForever,而这个动作的duration其实是0,并不是动画之间的间隔时间。这样,在它合成序列动作时,就只会执行前一个动作,之后便会忽略摆头动作。

解决办法很简单,对于Spawn,我们可以分开执行动作:

1
2
_cat->runAction(action1);
_cat->runAction(action2);

对于Sequence,我们可以将摆头动作加入回调函数,让回调函数执行动作,最后再合并成序列动作:

1
2
auto standCallFunc = CallFunc::create(CC_CALLBACK_0(Level1Layer::standCallBack, this));
Sequence *seq = Sequence::create(_cat->getActionScare(), standCallFunc, NULL);

回调函数如下:

1
2
3
4
void Level1Layer::standCallBack()
{
_cat->runAction(_cat->getActionStand());
}

这样,两个动作的切换便十分流畅了。

死亡动作

死亡动作的帧数有点多,切图的时候累死人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Animate *Cat::getActionDeath()
{
Animation *animation = Animation::create();
for (int i = 0; i != 15; i++)
{
char str[50];
sprintf(str, "cat/cat-death-%d.png", i);
animation->addSpriteFrameWithFileName(str);
}
animation->setDelayPerUnit(1.0 / 10.0);
Animate *animate = Animate::create(animation);
return animate;
}

传说中的死亡一指,一点就挂。