【cocos2D-x学习】13.code with box2D——是男人就下100层

【目标】:制作一个应用简单box2D功能的游戏——是男人就下100层


【参考】

一、box2D相关

如何使用cocos2d和box2d来制作一个Breakout游戏:第一部分  及其后续文章

二、cocos基础

cocos2d-x之区域裁剪

《IPhone & IPad 游戏开发实战》


         和前面的博客一样,限于时间问题,这里也不打算手把手的说明如何完整的写出这样一个游戏,只是重点说明前面没有说过的点。


扫描二维码关注公众号,回复: 3877767 查看本文章

第一部分:物理世界的搭建

一、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-碰撞剖析》之后,感觉还有很多可以提高的地方。计划会完全重写整个工程。

猜你喜欢

转载自blog.csdn.net/ronintao/article/details/14106217