【目标】:制作一个应用简单box2D功能的游戏——是男人就下100层
【参考】
一、box2D相关
如何使用cocos2d和box2d来制作一个Breakout游戏:第一部分 及其后续文章
二、cocos基础
《cocos2d-x之区域裁剪》
《IPhone & IPad 游戏开发实战》
和前面的博客一样,限于时间问题,这里也不打算手把手的说明如何完整的写出这样一个游戏,只是重点说明前面没有说过的点。
第一部分:物理世界的搭建
一、box2D的入门
这里我偷个懒,如果要学习最基础的box2D知识,那么上面子龙山人的译文、《box2d中文手册》,以及最重要的,Box2D提供的testBed Demo是最好的老师,我也没有办法讲的比那个更好了。
其实我本人,也是参照《如何使用box2d来做碰撞检测(且仅用来做碰撞检测)》来完成这个游戏的。这里我只说说,我这里所做的简单拓展。
在实际场景中,如果仅仅使用box2D来做碰撞检测,那么是用sprite的位置,来更新body的位置。这样就遇到一个问题:如果不同类型的sprite,他的body的相对位置不一样,该怎么处理呢?
举例来说,在我们这个游戏中,如果是一个下沉的白色陷阱板,那么为了使得他在运行下落帧的时候,顶部的位置不变,我会将其anchorPoint设置为 ccp(0.5f, 1.0f);如果是一个弹簧板,则他的底部是不变的,那么为了方便动画的运行,就会将其anchorPoint设置为 ccp(0.5f, 0.0f),而针对一般的板,则一般是默认的 ccp(0.5, 0.5)。
这个时候,如果用sprite的position来更新body的position,就会比较麻烦,解决的办法是为body也设置一个anchorPoint,类似于质心,然后在更新的时候,用这个质心来更新body:
设置质心mCentroid(这里的值,是相对于原sprite的锚点的距离),默认的都是0:
void ExtendSprite::initPhysical(b2World * world) { mWorld = world; if ( world != NULL ) mBody = addSquareSpriteIntoPhysical(world, this, 10.0f); mCentroid = CCPointZero; }如果是弹簧板,就要在锚点上方(这里是硬编码,因为图片高度是10,锚点在底部,那么 Y 方向移动5个像素,就是正中心)
void SpringBlock::initPhysical(b2World * world) { BlockBase::initPhysical(world); mCentroid = ccp(0, 5); }然后在更新的时候,取出质心:
for (b2Body * b = mWorld->GetBodyList(); b; b = b->GetNext()) { if ( b->GetUserData() != NULL ) { ExtendSprite * sprite = (ExtendSprite *)b->GetUserData(); //取该SRPITE的锚点在世界的位置,作为实际位置 CCPoint spritePos = sprite->convertToWorldSpaceAR(sprite->mCentroid); b2Vec2 position(spritePos.x/PTM_RATIO, spritePos.y/PTM_RATIO); float angle = -1 * CC_DEGREES_TO_RADIANS(sprite->getRotation()); b->SetTransform(position, angle); bodyList.push_back(b); } }
二、准备工作——启用box2D的debugDraw
对于我这样的新手来说,这一步工作非常重要。具体来说可以分为以下几步:
1、复制 GLES-Render.cpp
从cocos目录下的 samples\TestCpp\Classes\Box2DTestBed 路径找到 GLES-Render.cpp 和 GLES-Render.h,复制到你的工程下。
2、初始化 debugDraw
mTestDraw = new GLESDebugDraw(PTM_RATIO); uint32 flags = 0; flags += b2Draw::e_shapeBit ; flags += b2Draw::e_jointBit; //关节 flags += b2Draw::e_centerOfMassBit; //获取需要显示debugdraw的块 flags += b2Draw::e_aabbBit; //AABB块 flags += b2Draw::e_pairBit; mTestDraw->SetFlags(flags); mWorld->SetDebugDraw(mTestDraw);这里的 PTM_RATIO 是物理世界与像素界面的比例,需要你自己定义,所有使用处保持一致即可。另外 mWorld 是你需要事先创建好的world变量。
请注意,这里是new出来的,所以最后不要忘记释放。
3、绘制
覆写draw方法,(至于覆写谁的,谁持有这个world变量和testDraw变量,那么就是谁的,我这里是游戏层 gameLayer的),启用:
void PhysicalLayer::draw() { CCLayer::draw(); mWorld->DrawDebugData(); }
有的文章还写道,需要在step后执行 mWorld->DrawDebugData();,不过我实际测试下来似乎是不需要的。
这样,就可以看到和 box2D 的 testBed 相同的效果了。
三、基于box2D的碰撞检测
这一部分,建议阅读《Box2D C++教程16-碰撞剖析》,写的非常好。
简单来说,要使用box2D的碰撞检测,可以按照以下步骤:
1、写一个类继承 b2ContactListener
覆写其4个方法,我这里主要用到的是 BeginContact 和 EndContact
virtual void BeginContact(b2Contact* contact); virtual void EndContact(b2Contact* contact); virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold); virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
2、将这个listener设置给物理世界
这个很简单:
mContactListener = new RoninContactListener(); mWorld->SetContactListener(mContactListener);这个时候,我们在调用step的时候,如果发生了碰撞,就会在4个回调函数中收到消息了。
3、处理消息
我这里是仿照《如何使用box2d来做碰撞检测(且仅用来做碰撞检测)》中的做法,没有使用mWorld中提供的ContactList,而是在listener中维护两个列表,分别表示开始碰撞和结束碰撞的物体,然后在处理完之后,将其出栈:
首先维护两个列表:
void RoninContactListener::BeginContact(b2Contact* contact) { //cocos2d::CCLog("BeginContact"); RoninContact _contact = {contact->GetFixtureA(), contact->GetFixtureB()}; beginContactList.push_back(_contact); } void RoninContactListener::EndContact(b2Contact* contact) { //cocos2d::CCLog("EndContact"); RoninContact _contact = {contact->GetFixtureA(), contact->GetFixtureB()}; endContactList.push_back(_contact); }
在layer的update函数中,调用完step之后,处理碰撞对,并进行出栈操作:
void PhysicalLayer::update(float dt) { …………………… mWorld->Step(dt, 8, 1); //更新位置 std::vector<b2Body *> bodyList; for (b2Body * b = mWorld->GetBodyList(); b; b = b->GetNext()) { …………………… b->SetTransform(position, angle); } //触发碰撞监听 for ( ContactItor itor = mContactListener->beginContactList.begin(); itor != mContactListener->beginContactList.end(); itor ++ ) { //处理碰撞 ……………………………… } //清除beginContact队列 mContactListener->beginContactList.clear(); //和上面类似,处理EndContact列表 for ( ContactItor itor = mContactListener->endContactList.begin(); itor != mContactListener->endContactList.end(); itor ++ ) { ………………………… } mContactListener->endContactList.clear(); }
四、如何将自己从物理世界中移除
BeginContact和EndContact都是在step的时候回调的,但是这个时候世界正在进行迭代,所以绝对不能够在这个时候将自己从物理世界中移除,我们这时只能做一个标记。然后在后面检查这个变量,将所有不需要的元素从物理世界中移除。这个思路和上面的用一个列表记录contact类似,这里就不再赘述了。
第二部分:几个核心的cocos2D技术点
一、适配多种不同的分辨率
Android最大的麻烦之一,就在于各种奇葩的分辨率(这里必须要吐槽一下我司,简直是奇葩中的奇葩)。在上一个游戏(五子棋)中,我的办法是自己根据屏幕大小计算一个比例,将所有的sprite都进行等比例的缩放。这种方法实在是太麻烦了,以至于我再也不想使用这种方法了。幸而cocos2D已经为我们提供了一种解决方法:
你只需要在AppDelegate::applicationDidFinishLaunching()中添加这样一句:
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(320, 480, kResolutionShowAll);然后,就只需要考虑(大体上如此)320*480分辨率就可以了。
这里的第三个参数有几种选择:kResolutionExactFit、kResolutionNoBorder、kResolutionShowAll,在cocos2dx\platform\CCEGLViewProtocol.h中,你可以找到他们的定义以及注释。
如果选择我这里的参数kResolutionShowAll,就会完整显示整个界面,而在边界上留出黑边。举例来说,我这里设定的目标是320*480,如果手机分辨率是650*960,那么就会在其中间640*960部分完整显示我们的界面,并在左右两边各5像素的竖条区域显示黑边。
二、设定显示区域
这部分参考《cocos2d-x之区域裁剪》。由于和上面的内容相关,所以特别放在这里说明。
一般意义上,要裁剪显示区域,只需要在visit函数中启用裁剪测试GL_SCISSOR_TEST:
void GameArea::visit() { glEnable(GL_SCISSOR_TEST); CCRect visibleRect = getVisibleRect(); glScissor(visibleRect.origin.x, visibleRect.origin.y, visibleRect.size.width, visibleRect.size.height); CCNode::visit(); glDisable(GL_SCISSOR_TEST); }
但是需要特别注意一点,上面我们已经提到了如何适配多种分辨率。其实质类似于android中的dp和px关系。例如上面的320*480,到了640*960分辨率的手机上,cocos2d会把原来设置的一个单位映射到两个像素。但是直接调用openGL方法的时候,并不会进行这样的转换(毕竟不是openGL的机制),所以这里需要考虑到cocos2D的缩放,否则你永远只能显示320*480的大小的界面。
cocos2d提供了这样的接口,可以通过
CCEGLView::sharedOpenGLView()->getScaleX(); CCEGLView::sharedOpenGLView()->getScaleY();来获取。这一点在《 cocos2d-x之区域裁剪》中也是提到了。
最后,我们设定的裁剪可见区域如下:
CCRect GameArea::getVisibleRect() { CCPoint selfOrigin = convertToWorldSpace(CCPointZero); //For adapt on android float scaleX = CCEGLView::sharedOpenGLView()->getScaleX(); float scaleY = CCEGLView::sharedOpenGLView()->getScaleY(); CCRect viewPortRect = CCEGLView::sharedOpenGLView()->getViewPortRect(); CCRect visibleRect(selfOrigin.x * scaleX + viewPortRect.origin.x, selfOrigin.y * scaleY + viewPortRect.origin.y, WIDTH(this) * scaleX, HEIGHT(this) * scaleY); return visibleRect; }这里的WIDTH和HEIGHT是两个自定义的宏,分别是 contentSize 的高和宽。
三、重复纹理
bool ExtendSprite::initWithRepeatTex(const char * textureName, cocos2d::CCRect spriteRect) { initWithFile(textureName); setTextureRect(spriteRect); ccTexParams params = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT}; getTexture()->setTexParameters(¶ms); return true; }核心的几句是为纹理设置了拉伸参数GL_REPEAT,这样就可以使用一个小的纹理textureName来填充整个区域了。需要注意的是,由于openGL的限制(其实应该是早期版本的限制), 这里要求原始纹理的高和宽的大小是2的次方,比如8*16,而不能使用8*12这种。
四、视差移动——parallax
在cocos2d的TestCpp中,提供了一个这样的范例。可以使用CCParallaxNode来实现,不过这个类有些先天缺陷,比如不能实现我们下面要求的无限滚动,总归会有边界。
其实视差移动的核心的思想就是远近的物体以不同的速度移动,从而形成视差。所以我们可以自己实现,只需要画两层,使后面的那层慢速运动,前面的那层快速运动即可:
void GameArea::update(float dt) { ……………… //更新子元素的位置 for ( NodeItor itor = children.begin(); itor != children.end(); itor ++ ) { NodeExtend * node = *itor; //这里更新位置,前面层的元素被统一赋予速度50,后面层的元素被赋予速度30,从而形成了视差 node->updatePosition( 0, dt * node->speed ); ……………… } ………… }
五、无限滚动的背景
在我这个游戏中,由于背景实际上是单一的,所以要实现无限滚动,实际上只需要用两张同样的图片,交替往复的来回显示就可以了。当第一张图片移出界面的时候,将他下移一个身位,放到第二张图片下面,就可以实现无限的滚动显示:
void NoEndScrollSprite::updatePosition(float dx, float dy) { spriteA->updatePosition(dx, dy); spriteB->updatePosition(dx, dy); if ( spriteA->getPositionY() >= spriteHeight ) spriteA->updatePosition(0, -2 * spriteHeight); if ( spriteB->getPositionY() >= spriteHeight ) spriteB->updatePosition(0, -2 * spriteHeight); }
六、引入cocos2d::extension——用滚动条来做血条
这一段参考了另外一篇博客,不过他文风有点怪,就不贴出链接了。
血条在某种程度上,相当于弱化的滚动条CCControlSlider,不过少了一个控制按钮。因此我们可以利用cocos2d::extension来实现血条的功能。
1、引入cocos2d::extension
默认的VC模板中是没有添加这个模块的(android中倒是添加好了)。需要在工程属性-》C/C++ -》附加包含目录 中添加 F:\cocos\cocos2d-2.0-x-2.0.4\extensions (F:\cocos\cocos2d-2.0-x-2.0.4\是我放cocos的路径)。然后在需要的地方加上引用:
#include <cocos-ext.h>另外一个需要注意的是他和cocos2d主模块并不共享命名空间,所以需要使用 cocos2d::extension 限定,或是使用 USING_NS_CC_EXT 宏来使用该命名空间
2、创建血条
要创建一个滚动条,必须包含三个元素:
1)背景(就是血槽纹理),称之为tracker
2)血条,称之为 progress
3)控制钮,通过拉动他来进行滚动,称之为 thumb
作为血条,第三个元素肯定是没用的,所以给一个空的sprite就可以了。另一个需要注意的地方是不要让该滚动条响应touch事件,这样就彻底不可以拖了。
CCSprite * emptyThumb = CCSprite::create(); CCSprite * trackerSprite = CCSprite::create("lifebartracker.png"); CCSprite * progressSprite = CCSprite::create("lifebarprogress.png"); slider = CCControlSlider::create(trackerSprite, progressSprite, emptyThumb); slider->setTouchEnabled(false); slider->setMaximumValue(10.0f); slider->setMinimumValue(0.0f); slider->setValue(10.0f);
七、待解决的问题:拓展ccsprite
如果列位客官无聊到去看我的源码的话,会发现有一处比较奇怪的设计
有这样几个类,其关系如下:
在addChild和removeChild时,需要传入的参数都是CCObject类型的指针,现在如果要对原来的CCNode加一点扩展,并应用到CCSpriteBatchNode和CCSprite上,那么如果将CCObject * 强行 cast 成 NodeExtend * 指针,就会发现虽然所有的数据都复制过去了,但是虚函数表并没有复制,导致多态失败。
目前这个问题我还解决不了,规避的方法是不适用children作为迭代对象,自己建立了一个 NodeExtend 数组记录children,并且在 NodeExtend中保存一个 CCObject的指针,当在 addChild 或是 removeChild等需要使用 CCObject指针的时候来用。
这个问题先摆在这里,以后如果能力提升有办法解决了,再回来改写这一段。
第三部分:游戏的搭建
原来还和原来一样画个类图,不过最近实在有点累,就偷懒一次吧,不过代码还是OPEN SOURCE,C++,非OC,有兴趣的同学可以看看。
一、源码
http://download.csdn.net/detail/ronintao/6497171
二、to be continue
目前的实现,对box2d的利用还是不够充分,尤其在我看过《Box2D C++教程16-碰撞剖析》之后,感觉还有很多可以提高的地方。计划会完全重写整个工程。