🎨 CG | Cocos2D 的基本用法

本文主要描述了 Cocos2D 中精灵、场景等基本元素以及帧动画、调度器和键盘事件的使用方法。

🚀 代码: Github

参考资料

主要文档:

Coco2d-x 用户手册

API 文档

某些问题解决方案

How to remove sprite after sequence of animation?

安装

Cocos2d-x 依赖于 python2,虽然可以同时存在 python3 和 python2,可以通过py -2调用 python2,然而cocos这个脚本调用的是python命令,默认调用 python3,为了方便起见,就先把原来的 python3 删掉,装上 python2.

下载并解压cocos2d-x-3.16然后运行里面的setup.py

1
python ./setup.py

如果不需要配置 Android 环境,那么里面就是配置路径项就可以回车忽略掉

然后使得环境变量生效,就可以使用cocos命令了。

运行 cpp-tests 项目

cocos2d-x 目录下有一个cpp-tests, 里面有各种的示例代码,可以通过 Vs 打开它,并且改变一下各个解决方案的目标 SDK,然后就可以编译运行了。

TIM截图20180504182608

新建项目

1
cocos new helloCocos -p cn.zhenly.hellococos -l cpp
  • new : 项目名称
  • -p:包名
  • -l:开发语言
  • -d:项目存放目录(默认当前目录\项目名称)

打开项目

这里使用的 IDE 是 VS2017

使用 VS2017,打开新建项目目录下proj.win32 里面的helloCocos.sln

如果直接编译是会报错的。

打开之后,需要改变一下这里面的目标平台 SDK,因为里面的 SDK 默认是 8.1 的,我们要将他替换成我们环境中存在的。单击解决方案资源管理器,右键选择项目属性更改,5 个解决方案都需要改一下。

然后就可以编译运行了。

(第一次编译时间很久,可能是渣 CPU,而且编译完之后项目大小居然 4.57 GB,比一个系统镜像还要大)

添加元素

1525441682810

里面AppDelegate.cpp是程序的主入口,管理各种生命周期的

HelloWorldScene.cpp就是我们第一个场景,我们在里面添加一点元素

打开之后可以看到里面已经存在一些示例代码,放置了一个图片和文字,还有一个退出按钮,仿照他们就可以添加我们自己的元素。

在添加的时候发现中文的Label是不显示的,我使用了 xml 来加载中文字符。

首先在资源目录下\Resources新建一个Strings.xml

1
2
3
4
5
6
<dict>
<key>no</key>
<string>12345</string>
<key>name</key>
<string>你好,世界</string>
</dict>

这里有一点需要注意: 不能写成以下这种形式

1
2
3
<key>
no
</key>

虽然看上去是一样的,但是当获取名字为no的 key 的时候,发现返回的是NULL, 这是因为他的 key 名字把\n也包含进去了。

然后在代码中获取这些资源

1
2
3
auto *chnStrings = Dictionary::createWithContentsOfFile("Strings.xml");
const char *strName = ((String*)chnStrings->objectForKey("name"))->getCString();
const char *strNo = ((String*)chnStrings->objectForKey("no"))->getCString();

然后放到label里面

1
2
auto labelNo = Label::createWithTTF(strNo, "fonts/Marker Felt.ttf", 24);
auto labelName = Label::createWithSystemFont(strName, "Microsoft YaHei UI", 24);

然后我们再来添加一个菜单项

它的示例里面已经有添加一个关闭的图片按钮,我再添加一个label按钮

1
2
3
4
5
6
7
8
9
10
11
12
// add a "about" label to show about info.
auto labelAbout = Label::createWithTTF("Show", "fonts/Marker Felt.ttf", 24);
auto aboutItem = MenuItemLabel::create(labelAbout,
CC_CALLBACK_1(HelloWorld::menuAboutCallback, this));
if (labelAbout == nullptr || aboutItem == nullptr) {
problemLoading("'fonts/Marker Felt.ttf'");
}
else {
float x = origin.x + visibleSize.width - closeItem->getContentSize().width - aboutItem->getContentSize().width;
float y = origin.y + aboutItem->getContentSize().height / 2 + 8;
aboutItem->setPosition(Vec2(x, y));
}

然后把他添加到 Menu 里面就可以了, 是不是很简单

1
2
3
4
5

// create menu, it's an autorelease object
auto menu = Menu::create(closeItem, aboutItem, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);

1525442252880


这个星期 Cocos 是做一个并没有黄金矿工的黄金矿工。

首先这个提供了一个 Demo,和素材,这样做起来就十分地舒服

开始场景

这一部分需要添加三个东西进去,分别是标题,大石头和开始按钮

标题和大石头都是普通的 Sprite,因此用最普通的方法,计算好他们的坐标,然后添加进去就可以了。

然后开始按钮要求是按下是有一定的交互的,因此可以使用MenuItemImage来实现

1
2
3
4
5
6
auto startButton = MenuItemImage::create("start-0.png", "start-1.png",
CC_CALLBACK_1(MenuScene::startMenuCallback, this));
startButton->setPosition(Vec2(visibleSize.width + origin.x - 203, origin.y + 193));
auto menu = Menu::create(startButton, NULL);
menu->setPosition(Vec2::ZERO);
this->addChild(menu, 1);

这些都是十分常规的操作。

然后切换场景也是十分地简单, 只需要调用Director的实例的对应的replaceScene方法,就可以完成了。

甚至我们还可以加一些特效。TranstionFlipX 就是以 x 轴为中心平面式地旋转切换。

1
2
3
void MenuScene::startMenuCallback(cocos2d::Ref * pSender){
Director::getInstance()->replaceScene(TransitionFlipX::create(1, GameSence::createScene()));
}

然后就可以得出以下效果

1526567021405

游戏场景

首先就是要布置场景,需要放置的有一只会动的老鼠,一块石头,一个背景还有一个 Shoot 的菜单按钮

背景和石头都是普通的 Sprite, Shoot 和上一个场景的 Start 是类似的,不过换成了一个Label而已,代码也是大同小异的,重要的是那只会动的老鼠。

不过这部分需要使用到两个Layer, 这就需要额外设置一下锚点和位置,只要调用setAnchorPointsetPosition方法就可以了。

至于这只老鼠,我们可以仿照在上一个场景中那个抖脚矿工的动画,在AppDelegate.cpp中加载plist资源,然后把动画存在AnimationCache当中,也是很容易就搞定了。

1
2
3
4
5
6
7
8
9
10
11
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("level-sheet.plist");
char mouseFrames = 7;
char mouseFramesName[30];
Animation* mouseAnimation = Animation::create();

for (int i = 0; i < mouseFrames; i++) {
sprintf(mouseFramesName, "pulled-gem-mouse-%d.png", i);
mouseAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName(mouseFramesName));
}
mouseAnimation->setDelayPerUnit(0.1);
AnimationCache::getInstance()->addAnimation(mouseAnimation, "mouseAnimation");

然后在GameScene.cpp中加入

1
2
3
4
5
6
auto mouse = Sprite::createWithSpriteFrameName("pulled-gem-mouse-0.png");
Animate* mouseAnimate = Animate::create(AnimationCache::getInstance()->getAnimation("mouseAnimation"));
mouse->runAction(RepeatForever::create(mouseAnimate));
mouse->setPosition(visibleSize.width / 2, 0);
mouse->setName("mouse");
mouseLayer->addChild(mouse, 2);

然后就可以得到这个场景了:

1526567037775

然后触摸事件里面添加一些方法,使得点击的地方生成一个奶酪并且老鼠会移动过去。

因为我的老鼠是在mouseLayer里面的,这就涉及到了局部坐标和世界坐标的转换。

这个只需要调用对应layerconvertToNodeSpace方法就可以了

1
auto gotoCheese = MoveTo::create(5.0f, mouseLayer->convertToNodeSpace(Vec2(location)));

而奶酪需要出现后等待一定时候后淡出,而且还需要移除,因此我们可以使用一个Sequence来实现

1
2
3
4
5
6
7
auto newCheese = Sprite::create("cheese.png");
newCheese->setPosition(location);
auto fadeOut = FadeOut::create(5.0f);
auto delay = DelayTime::create(1.0f);
auto action = Sequence::create(delay, fadeOut, RemoveSelf::create(), nullptr);
newCheese->runAction(action);
this->addChild(newCheese, 1);

然后就可以完成这个部分

1526567373987

最后需要发射石头到老鼠位置,并且老鼠留下钻石并随机逃跑。

这个和上面老鼠移动到奶酪位置的做法是差不多的,不过这个需要生成一个指定范围的随机坐标。

这个使用C++11中的Random库就可以搞定

1
2
3
4
5
6
7
std::default_random_engine randomEngine(time(NULL));
Vec2 GameSence::getRandomVec2() {
Size visibleSize = Director::getInstance()->getVisibleSize();
std::uniform_real_distribution<float> disW(30.0, visibleSize.width - 60);
std::uniform_real_distribution<float> disH(20.0, visibleSize.height - 200);
return Vec2(disW(randomEngine), disH(randomEngine));
}

我这里加了限制使得老鼠逃离的地方在泥土里面。

然后就可以实现最后的功能了。

1526567393546

这个项目做到这里基本就完成了,不过还有一个小问题,就是如果你在老鼠移动未完成的时候再次执行移动的动作,那么老鼠最终到达的位置就不会是指定的位置,这是因为他的移动未完成之前,再次调用移动就会导致新的移动的坐标叠加旧的移动的目标地址上,也就是说,如果在一个地方同时点击两次,那么老鼠就会移动两倍的距离,这是非常影响体验的。因此我这里作于一个改进

1
2
3
4
5
6
7
8
9
10
auto moveMouse = MoveTo::create(3.0f, mouseLayer->convertToNodeSpace(getRandomVec2()));
auto moveEaseIn = EaseElasticOut::create(moveMouse);
if (mouse->getActionByTag(634) != nullptr) {
mouse->stopActionByTag(634);
}
if (mouse->getActionByTag(534) != nullptr) {
mouse->stopActionByTag(534);
}
moveEaseIn->setTag(634);
mouse->runAction(moveEaseIn);

就是给老鼠移动到奶酪或者移动到新地址的动作加上一个tag,然后再次执行移动的时候,先通过tag将当前的动作停止,然后老鼠的坐标就会被设置到当前位置,新的移动的坐标就会被叠加到当前位置上,就可以解决这个问题了。


这次的项目的要求比较简单,就是一个简单的横板游戏(虽然并没有什么可以玩的。涉及到的关键点也不多,因此很快就可以完成了。

布局

血条

这个是素材当中的一个元素,这里可以通过从素材图片中截取指定的矩形来提取出来。提取出来后得到一个血条的背景和一段粉红色的条。

1
2
Sprite* sp0 = Sprite::create("hp.png", CC_RECT_PIXELS_TO_POINTS(Rect(0, 320, 420, 47)));
Sprite* sp = Sprite::create("hp.png", CC_RECT_PIXELS_TO_POINTS(Rect(610, 362, 4, 16)));

然后就是对于一些类型和位置的设置

1
2
3
4
5
6
7
8
9
10
11
12
pT = ProgressTimer::create(sp);
pT->setScaleX(90);
pT->setAnchorPoint(Vec2(0, 0));
pT->setType(ProgressTimerType::BAR);
pT->setBarChangeRate(Point(1, 0));
pT->setMidpoint(Point(0, 1));
pT->setPercentage(100);
pT->setPosition(Vec2(origin.x + 14 * pT->getContentSize().width, origin.y + visibleSize.height - 2 * pT->getContentSize().height));
addChild(pT, 1);
sp0->setAnchorPoint(Vec2(0, 0));
sp0->setPosition(Vec2(origin.x + pT->getContentSize().width, origin.y + visibleSize.height - sp0->getContentSize().height));
addChild(sp0, 0);

角色人物

这里的素材是从一段帧动画从提取的第一帧画面,首先创建一个贴图,然后切割出一个关键帧,从而创建一个精灵,那么就可以了

1
2
3
4
5
6
7
8
9
//创建一张贴图
auto texture = Director::getInstance()->getTextureCache()->addImage("$lucia_2.png");
//从贴图中以像素单位切割,创建关键帧
auto frame0 = SpriteFrame::createWithTexture(texture, CC_RECT_PIXELS_TO_POINTS(Rect(0, 0, 113, 113)));
//使用第一帧创建精灵
player = Sprite::createWithSpriteFrame(frame0);
player->setPosition(Vec2(origin.x + visibleSize.width / 2,
origin.y + visibleSize.height / 2));
addChild(player, 3);

控制按钮

控制按钮由MenuItemLabel组成,首先是使用Label::createWithTTF从指定的ttf文件创建Label, 然后绑定指定的回调事件,最后添加到Menu里面,就可以搞定了。

这里比较需要关注的是回调函数,因为这 6 个按钮可以分为两种类型,因此可以设置两个回调函数,然后根据参数的不同而呈现不同的动作,这样就可以避免大量的重复代码

这里举一个例子:

1
2
3
4
5
6
// .cpp
auto buttonW = MenuItemLabel::create(labelW, CC_CALLBACK_1(HelloWorld::buttonWASDCallBack, this, 0));

// .h
/// <param name='dir'>方向: 0 - w, 1 - a, 2 - s, 3 - d</param>
virtual void buttonWASDCallBack(Ref* pSender, int dir);

这里将WASD的移动动作写到一个函数里面,然后根据不同的参数决定不同的移动方向.

这里使用的回调类型为CC_CALLBACK_1, 这个 1 的意思就是这个函数有一个自定义的参数,同样的有0, 2, 3等其他回调类型。

动画

帧动画

这里使用到一些帧动画,是由一张图片切割出不同的帧组成的。

至于具体的切割操作也是比较简单

比如说是死亡动画

1
2
3
4
5
6
7
8
// 死亡动画(帧数:22帧,高:90,宽:79)
auto texture2 = Director::getInstance()->getTextureCache()->addImage("$lucia_dead.png");
int frameCount = 22;
dead.reserve(frameCount);
for (int i = 0; i < frameCount; i++) {
auto frame = SpriteFrame::createWithTexture(texture2, CC_RECT_PIXELS_TO_POINTS(Rect(79 * i, 0, 79, 90)));
dead.pushBack(frame);
}

只要知道这个素材的帧数和高宽,就可以轻易生成一个动画了。

之后的使用方法:

1
2
auto animation = Animation::createWithSpriteFrames(run, 0.05f);
player->runAction(Animate::create(animation));

从这个容器里面加载出素材,指定速度创建一个Animation,然后再创建为Animate就可以了。

防止动画重复

因为有些动画在逻辑上是不应该同时或者重复发生的,因此需要做一些措施防止他们重复发生。

最简单的方法就是给指定的动画通过setTag设置一个Tag,然后通过调用getActionByTag(xxx)->isDone()判断他们是否执行完成再进行下一步操作。

1
2
3
4
if (player->getActionByTag(534) != nullptr && !(player->getActionByTag(534)->isDone())) return;
// ....
action->setTag(534);
player->runAction(action);

同时发生的动画

对于移动操作,我们需要走路动画和移动动画同时发生,因此需要两个Animate同时发生,因此可以使用Spawn创建一个同时序列

1
auto action = Spawn::createWithTwoActions(Animate::create(animation), move);

调度器

这次项目要求显示一个倒计时,这个可以通过自定义一个 1s 的调度器实现。

调度器的生成

1
2
void updateTime(float dt);
this->schedule(schedule_selector(HelloWorld::updateTime), 1.0f, kRepeatForever, 0);

调度器的停止

1
this->unschedule(schedule_selector(HelloWorld::updateTime));

需要注意的是,调度器中的函数的参数必须是 float,否则会报错。

键盘事件

由于是横板游戏,在 PC 端肯定是需要加入一些键盘事件的,不然总是鼠标点击会异常反人类

创建键盘事件的方法也比较简单,如下:

1
2
3
4
5
6
7
8
9
10
// 键盘事件
auto eventListener = EventListenerKeyboard::create();

eventListener->onKeyPressed = [this](EventKeyboard::KeyCode keyCode, Event* event) {
switch (keyCode) {
case EventKeyboard::KeyCode::KEY_LEFT_ARROW:
case EventKeyboard::KeyCode::KEY_A:
// ...
}
};

结果

1527226036810

1527226049329

1527226068990

亮点与改进

  • 第一个项目
    • 为文字添加了不同的样式(颜色、阴影、发光)
    • 点击回调事件生成了一个特效
  • 第二个项目
    • 为矿工的嘴加上了动画
    • 为老鼠的移动加上了特效(EaseElasticOut
    • 为钻石加上了抖动特效
    • 解决了 Demo 中老鼠会移动到界面之外的feature
  • 第三个项目
    • 加上了键盘事件
    • 实现了血条的增加和减少

遇到的问题

老鼠连续移动超出界面

如果你在老鼠移动未完成的时候再次执行移动的动作,那么老鼠最终到达的位置就不会是指定的位置,这是因为他的移动未完成之前,再次调用移动就会导致新的移动的坐标叠加旧的移动的目标地址上,也就是说,如果在一个地方同时点击两次,那么老鼠就会移动两倍的距离,这是非常影响体验的。因此我这里作于一个改进

1
2
3
4
5
6
7
8
9
10
auto moveMouse = MoveTo::create(3.0f, mouseLayer->convertToNodeSpace(getRandomVec2()));
auto moveEaseIn = EaseElasticOut::create(moveMouse);
if (mouse->getActionByTag(634) != nullptr) {
mouse->stopActionByTag(634);
}
if (mouse->getActionByTag(534) != nullptr) {
mouse->stopActionByTag(534);
}
moveEaseIn->setTag(634);
mouse->runAction(moveEaseIn);

就是给老鼠移动到奶酪或者移动到新地址的动作加上一个tag,然后再次执行移动的时候,先通过tag将当前的动作停止,然后老鼠的坐标就会被设置到当前位置,新的移动的坐标就会被叠加到当前位置上,就可以解决这个问题了。

不同帧动画的素材宽高不一致

因为移动的动画的素材的宽高和攻击动画的素材的宽高不同,然后一开始的静态动画是攻击的素材的第一帧,这个问题导致了人物在移动的时候会发生一次瞬移, 而且每当切换攻击和移动动画的时候都会发生一次的瞬移。一开始我想通过MoveTo解决的,但是导致逻辑变得复杂,而且效果也不是很好,因此需要解决这个问题最好的方法就是换素材了。

思考与总结

​ 这次的实验是使用 Cocos2d-x 引擎制作游戏,而且使用的是C++的版本。虽然C++没有Js那样灵活,但是它终究是一款编译型语言,具有强类型和编译时检查,更容易发现出 BUG。

​ 对于游戏来说,Cocos2d-x 的布局比UWPxmal布局更为灵活,也更加符合游戏。应用需要整齐规划的节目,而游戏更需要动态的节目。虽然根据坐标布局起来的确有点麻烦,但是如果在一开始设计的时候就把坐标包括在内,那么游戏开发的过程中就会变得很方便,只需要加载预先的设置,比如说存在plist中的坐标之类的。对于一个scene里面的元素,也可以预先设计好相应元素的坐标,那么就会很舒服。

​ 虽然在业界对于 Cocos2d-x 的评价也不是比较好,但是对于游戏应用的入门倒是足够简单,提供的基本功能也够开发一些小游戏了。但是对于一些规模大一点的游戏,这个架构似乎就驾驭不起了。希望后续版本可以做到足够好吧。

​ 由于我才写了三个项目,对于 Cocos2d-x 的理解并不是很深刻,也没有什么更加高深的见解,还是以后不断积累经验,再做更详细的总结吧。

​ 其实我们学习这个框架也不是为了学习这个框架,只是为了学习一些游戏制作的一些理念和模式而已,就像 UWP 一样,和各种平台上的应用开发都有这很多的相似之处,只要掌握了方法,什么框架还驾驭不起呢。

​ 不知道说些什么了,就这样吧。

土豪通道
0%