在第二篇《如何制作一个类似Tiny Wings的游戏》基础上,增加添加主角,并且使用Box2D来模拟主角移动,原文《How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 2》,在这里继续以Cocos2d-x进行实现。有关源码、资源等在文章下面给出了地址。
步骤如下:
1.使用上一篇的工程;
2.创建Box2D世界,并且添加一些代码来进行调试绘制,同时添加一些形状。打开HelloWorldScene.h文件,添加如下定义:
1
|
#define PTM_RATIO
32.
0
|
然后,添加如下变量:
1
|
b2World *_world;
|
这里声明了一个像素到米的转换比率,为32.0。这个是用来进行Box2D单位(米)和Cocos2D单位(点)之间的转换。另外,还定义了一个世界对象_world变量。打开HelloWorldScene.cpp文件,添加如下方法:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void HelloWorld::setupWorld()
{ b2Vec2 gravity = b2Vec2( 0.0f, - 7.0f); bool doSleep = true; _world = new b2World(gravity); _world->SetAllowSleeping(doSleep); } void HelloWorld::createTestBodyAtPostition(CCPoint position) { b2BodyDef testBodyDef; testBodyDef.type = b2_dynamicBody; testBodyDef.position.Set(position.x / PTM_RATIO, position.y / PTM_RATIO); b2Body *testBody = _world->CreateBody(&testBodyDef); b2CircleShape testBodyShape; b2FixtureDef testFixtureDef; testBodyShape.m_radius = 25. 0 / PTM_RATIO; testFixtureDef.shape = &testBodyShape; testFixtureDef.density = 1. 0; testFixtureDef.friction = 2. 0; testFixtureDef.restitution = 0. 5; testBody->CreateFixture(&testFixtureDef); } |
这里setupWorld方法创建了一个有重力的世界,略小于地球标准-9.8米/秒2。createTestBodyAtPostition方法创建了一个测试对象,即一个25点半径的圆。在每次触摸屏幕时,都将使用这个方法来创建测试对象,但这仅是用来测试。在onEnter方法里面,最上面添加如下代码:
1
|
this->setupWorld();
|
修改_terrain = Terrain::create();为如下代码:
1
|
_terrain = Terrain::createWithWorld(_world);
|
这里调用了创建重力世界的方法,还修改了创建地形类的方法,传递了Box2D世界对象。这样山丘就可以使用这个对象来创建Box2D物体。在update方法里面,最上面添加如下代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static
double UPDATE_INTERVAL =
1.0f /
60.0f;
static double MAX_CYCLES_PER_FRAME = 5; static double timeAccumulator = 0; timeAccumulator += dt; if (timeAccumulator > (MAX_CYCLES_PER_FRAME * UPDATE_INTERVAL)) { timeAccumulator = UPDATE_INTERVAL; } int32 velocityIterations = 3; int32 positionIterations = 2; while (timeAccumulator >= UPDATE_INTERVAL) { timeAccumulator -= UPDATE_INTERVAL; _world->Step(UPDATE_INTERVAL, velocityIterations, positionIterations); _world->ClearForces(); } |
这里每次更新,调用了_world->Step,来运行Box2D的物理仿真。注意,这里使用固定时间步长来实现运行物理仿真,而这优于可变时间步长。在ccTouchesBegan方法里面,最后面添加如下代码:
1
2 |
CCTouch *anyTouch =
static_cast<CCTouch*>(pTouches->anyObject());
CCPoint touchLocation = _terrain->convertTouchToNodeSpace(anyTouch); this->createTestBodyAtPostition(touchLocation); |
这个方法用来当触摸屏幕的时候,创建一个Box2D物体。另外,这仅仅是为了测试Box2D是否能够正常运行。注意,这里得到的触摸坐标是在地形坐标内。这是因为地形会滚动,而所想要的是地形内的位置,而不是屏幕上的位置。打开Terrain.h文件,添加头文件声明,代码如下:
1
2 |
#include
"Box2D/Box2D.h"
#include "GLES-Render.h" |
GLES-Render用来实现Box2D的可调试绘制,此文件位于“\cocos2d-x-2.1.4\samples\Cpp\TestCpp\Classes\Box2DTestBed”,拷贝GLES-Render.h、GLES-Render.cpp文件到工程目录,添加进工程。在Terrain.h文件里,添加如下代码:
1
2 3 4 5 |
static Terrain *createWithWorld(b2World *world);
b2World *_world; b2Body *_body; GLESDebugDraw *_debugDraw; |
打开Terrain.cpp文件,添加头文件声明,代码如下:
1
|
#include
"HelloWorldScene.h"
|
在构造函数里面,添加如下代码:
1
2 3 |
_world =
NULL;
_body = NULL; _debugDraw = NULL; |
添加如下方法:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void Terrain::resetBox2DBody()
{ if (_body) { return; } CCPoint p0 = _hillKeyPoints[ 0]; CCPoint p1 = _hillKeyPoints[kMaxHillKeyPoints - 1]; b2BodyDef bd; bd.position.Set( 0, 0); _body = _world->CreateBody(&bd); b2EdgeShape shape; b2Vec2 ep1 = b2Vec2(p0.x / PTM_RATIO, 0); b2Vec2 ep2 = b2Vec2(p1.x / PTM_RATIO, 0); shape.Set(ep1, ep2); _body->CreateFixture(&shape, 0); } |
这仅仅是一个辅助方法,用来创建沿着山丘底部的一个Box2D物体,代表“地面”。因此可以让所添加的物体与“地面”物体进行碰撞,而不是无休止地掉落。这只是一个临时的方法,稍后会修改成山丘模型。就目前而言,它所做的只是将第一个和最后一个顶峰点的x轴坐标,用一条边连接起来。接下来,在resetHillVertices方法里面,在代码prevToKeyPointI = _toKeyPointI;之后,添加如下代码:
1
|
this->resetBox2DBody();
|
每当山丘顶点被重置的时候,调用resetBox2DBody方法来创建Box2D物体,此物体代表可见部分的山丘。眼下,这个物体没有进行改变(它只是添加了一条线,当作地面),但后面会进行修改这个物体为可见部分山丘的模型。继续添加如下方法:
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 |
Terrain * Terrain::createWithWorld(b2World *world)
{ Terrain *pRet = new Terrain(); if (pRet && pRet->initWithWorld(world)) { pRet->autorelease(); } else { CC_SAFE_DELETE(pRet); } return pRet; } bool Terrain::initWithWorld(b2World *world) { bool bRet = false; do { CC_BREAK_IF(!CCNode::init()); _world = world; this->setupDebugDraw(); this->setShaderProgram(CCShaderCache::sharedShaderCache()->programForKey(kCCShader_PositionTexture)); this->generateHills(); this->resetHillVertices(); bRet = true; } while ( 0); return bRet; } void Terrain::setupDebugDraw() { _debugDraw = new GLESDebugDraw(PTM_RATIO); _world->SetDebugDraw(_debugDraw); _debugDraw->SetFlags(GLESDebugDraw::e_shapeBit | GLESDebugDraw::e_jointBit); } |
然后在draw方法里面,最后面添加如下代码:
1
|
_world->DrawDebugData();
|
setupDebugDraw方法和draw方法里面调用DrawDebugData方法,是设置可调试绘制Box2D对象所必需的。另外,将可调试绘制代码放在Terrain.cpp文件中,而不是HelloWorldScene.cpp文件中,是因为这个游戏的滚动效果是通过移动地形来实现的。因此,要将Box2D中的坐标系与屏幕的可见坐标系匹配起来,就需要将可调试绘制代码放在Terrain.cpp文件中。最后,编译运行,触摸屏幕,就可以看到圆形的物体落入场景里面,如下图所示:
3.Box2D中塑造山丘。现在已经有一个Box2D的形状来代表屏幕底部的地面,但真正想要的是一个代表山丘的形状。幸运的是,这很容易办到,因为已经有足够的先前条件:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void Terrain::resetBox2DBody()
{ if (_body) { _world->DestroyBody(_body); } b2BodyDef bd; bd.position.Set( 0, 0); _body = _world->CreateBody(&bd); b2EdgeShape shape; b2Vec2 p1, p2; for ( int i = 0; i < _nBorderVertices - 1; ++i) { p1 = b2Vec2(_borderVertices[i].x / PTM_RATIO, _borderVertices[i].y / PTM_RATIO); p2 = b2Vec2(_borderVertices[i + 1].x / PTM_RATIO, _borderVertices[i + 1].y / PTM_RATIO); shape.Set(p1, p2); _body->CreateFixture(&shape, 0); } } |
这种新的实现方式,首先检查是否已经存在Box2D物体,如果是的话,就销毁原来的物体。然后,创建一个新的物体,循环遍历山丘顶点数组,对于每相邻的两个点,创建一条边进行连接起来。编译运行,就可以看到沿着山丘斜坡,有一个Box2D物体,如下图所示:
4.添加海豹。下载本游戏所需的资源,将资源放置"Resources"目录下。添加英雄类Hero,派生自CCSprite类。文件Hero.h代码如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#pragma once
#include "cocos2d.h" #include "Box2D/Box2D.h" class Hero : public cocos2d::CCSprite { public: Hero( void); ~Hero( void); static Hero *createWithWorld(b2World *world); bool initWithWorld(b2World *world); void update( float dt); void createBody(); CC_SYNTHESIZE_READONLY(bool, _awake, Awake); private: b2World *_world; b2Body *_body; }; |
这里只是声明了Box2D头文件,然后定义了一些方法和变量。文件Hero.cpp代码如下:
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
#include
"Hero.h"
#include "HelloWorldScene.h" using namespace cocos2d; Hero::Hero( void) { _world = NULL; _body = NULL; _awake = false; } Hero::~Hero( void) { } Hero * Hero::createWithWorld(b2World *world) { Hero *pRet = new Hero(); if (pRet && pRet->initWithWorld(world)) { pRet->autorelease(); } else { CC_SAFE_DELETE(pRet); } return pRet; } bool Hero::initWithWorld(b2World *world) { bool bRet = false; do { CC_BREAK_IF(!CCSprite::initWithSpriteFrameName( "seal1.png")); _world = world; this->createBody(); bRet = true; } while ( 0); return bRet; } void Hero::update( float dt) { this->setPosition(ccp(_body->GetPosition().x * PTM_RATIO, _body->GetPosition().y * PTM_RATIO)); b2Vec2 vel = _body->GetLinearVelocity(); b2Vec2 weightedVel = vel; float angle = ccpToAngle(ccp(vel.x, vel.y)); if (_awake) { this->setRotation(- 1 * CC_RADIANS_TO_DEGREES(angle)); } } void Hero::createBody() { float radius = 16.0f; CCSize size = CCDirector::sharedDirector()->getWinSize(); int screenH = size.height; CCPoint startPosition = ccp( 0, screenH / 2 + radius); b2BodyDef bd; bd.type = b2_dynamicBody; bd.linearDamping = 0.1f; bd.fixedRotation = true; bd.position.Set(startPosition.x / PTM_RATIO, startPosition.y / PTM_RATIO); _body = _world->CreateBody(&bd); b2CircleShape shape; shape.m_radius = radius / PTM_RATIO; b2FixtureDef fd; fd.shape = &shape; fd.density = 1.0f / CC_CONTENT_SCALE_FACTOR(); fd.restitution = 0.0f; fd.friction = 0.2f; _body->CreateFixture(&fd); } |
这个createBody方法创建了一个圆形的形状来代表海豹。这与之前写的createTestBodyAtPosition方法几乎完全一样,除了半径是基于海豹大小(实际上小于海豹大小,这样碰撞检测会更好)。此外,摩擦设置为0.2(因此海豹是非常滑的),恢复设置为0(使得海豹撞击地面时,不会反弹)。还设置了物体的一些线性阻尼,所以海豹随着时间推移会慢慢减缓速度,并且设置了物体为固定旋转,因为这个游戏并不需要它进行旋转。initWithWorld方法初始化精灵为特定的精灵帧(seal1.png),保存世界对象指针,然后调用createBody方法。update方法以Box2D物体的位置来更新海豹精灵的位置,同时根据物体的速度来更新海豹的旋转。打开Terrain.h文件,添加如下代码:
1
|
CC_SYNTHESIZE_RETAIN(cocos2d::CCSpriteBatchNode*, _batchNode, BatchNode);
|
打开Terrain.cpp文件,在initWithWorld方法里面,最后面添加如下代码:
1
2 3 |
_batchNode = CCSpriteBatchNode::create(
"TinySeal.png");
this->addChild(_batchNode); CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile( "TinySeal.plist"); |
这里只是简单地为TinySeal.png精灵表单创建批处理节点,然后从TinySeal.plist加载精灵帧定义,放入精灵帧缓存。打开HelloWorldScene.h文件,添加头文件声明:
1
|
#include
"Hero.h"
|
然后,添加如下变量:
1
|
Hero *_hero;
|
打开HelloWorldScene.cpp文件,在onEnter方法里面,最后面添加如下代码:
1
2 |
_hero = Hero::createWithWorld(_world);
_terrain->getBatchNode()->addChild(_hero); |
在update方法里面,在代码float PIXELS_PER_SECOND = 100;开始,修改为如下代码:
1
2 3 4 5 |
//float PIXELS_PER_SECOND = 100; //static float offset = 0; //offset += PIXELS_PER_SECOND * dt; _hero->update(dt); float offset = _hero->getPosition().x; |
编译运行,现在可以看到一只海豹在屏幕左边,如下图所示:
但是,有一些奇怪,海豹的一部分在屏幕之外。将它往右边移动一些,将会更好。打开Terrain.cpp文件,修改setOffsetX方法为如下:
1
2 3 4 5 6 7 8 |
void Terrain::setOffsetX(
float newOffsetX)
{ CCSize winSize = CCDirector::sharedDirector()->getWinSize(); _offsetX = newOffsetX; this->setPosition(ccp(winSize.width / 8 - _offsetX * this->getScale(), 0)); this->resetHillVertices(); } |
这里增加屏幕宽度1/8的偏移,使得海豹可以出现在右边一些。编译运行,现在可以看到海豹的样子了,如下图所示:
5.使海豹移动。采取的策略如下:
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 |
void Hero::wake()
{ _awake = true; _body->SetActive( true); _body->ApplyLinearImpulse(b2Vec2( 1, 2), _body->GetPosition()); } void Hero::dive() { _body->ApplyForce(b2Vec2( 5, - 50), _body->GetPosition()); } void Hero::limitVelocity() { if (!_awake) { return; } const float minVelocityX = 5; const float minVelocityY = - 40; b2Vec2 vel = _body->GetLinearVelocity(); if (vel.x < minVelocityX) { vel.x = minVelocityX; } if (vel.y < minVelocityY) { vel.y = minVelocityY; } _body->SetLinearVelocity(vel); } |
wake方法对物体应用了一个右上角的冲量,使得海豹开始移动。dive方法对物体应用了一个强烈的向下冲量,和一个稍微向右的冲量。向下的冲量会导致海豹撞上山丘,山丘斜坡越大的,一旦到达下一个山头,海豹将会飞得更高。limitVelocity方法确保海豹的移动速度,在x轴方向至少5m/s2,在y轴方向不小于-40m/s2。最后,打开HelloWorldScene.h文件,添加如下变量:
1
|
bool _tapDown;
|
打开HelloWorldScene.cpp文件,在构造函数里面,添加如下代码:
1
|
_tapDown =
false;
|
在update方法里面,最上面添加如下代码:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
if (_tapDown)
{ if (!_hero->getAwake()) { _hero->wake(); _tapDown = false; } else { _hero->dive(); } } _hero->limitVelocity(); |
修改ccTouchesBegan方法,为如下代码:
1
2 3 4 5 |
void HelloWorld::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent)
{ this->genBackground(); _tapDown = true; } |
添加如下方法:
1
2 3 4 5 6 7 8 9 |
void HelloWorld::ccTouchesEnded(CCSet *pTouches, CCEvent *pEvent)
{ _tapDown = false; } void HelloWorld::ccTouchesCancelled(CCSet *pTouches, CCEvent *pEvent) { _tapDown = false; } |
编译运行,现在可以看到海豹飞了起来,如下图所示:
6.修复摇晃的海豹。可能会注意到,当海豹沿着地面滑动的时候,会出现身体摇晃的情况。解决问题的一种方法是采用之前的线性速度与当前速度的加权平均值。打开Hero.h文件,添加如下代码:
1
|
#define NUM_PREV_VELS
5
|
添加如下变量:
1
2 |
b2Vec2 _prevVels[NUM_PREV_VELS];
int _nextVel; |
打开Hero.cpp文件,在构造函数里面,添加如下代码:
1
2 |
memset(_prevVels,
0, NUM_PREV_VELS *
sizeof(b2Vec2));
_nextVel = 0; |
修改update方法为如下:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void Hero::update(
float dt)
{ this->setPosition(ccp(_body->GetPosition().x * PTM_RATIO, _body->GetPosition().y * PTM_RATIO)); b2Vec2 vel = _body->GetLinearVelocity(); b2Vec2 weightedVel = vel; for ( int i = 0; i < NUM_PREV_VELS; ++i) { weightedVel += _prevVels[i]; } weightedVel = b2Vec2(weightedVel.x / NUM_PREV_VELS, weightedVel.y / NUM_PREV_VELS); _prevVels[_nextVel++] = vel; if (_nextVel >= NUM_PREV_VELS) { _nextVel = 0; } float angle = ccpToAngle(ccp(weightedVel.x, weightedVel.y)); if (_awake) { this->setRotation(- 1 * CC_RADIANS_TO_DEGREES(angle)); } } |
这里采用之前的5个速度做加权平均值,而不是仅仅使用线性速度。编译运行,现在可以看到一个滑动更加顺畅的海豹了,如下图所示:
7.缩小画面。Tiny Wings游戏中有一个很酷的效果,就是飞的越高,屏幕画面就会越小。这不仅可以使英雄一直在画面中,而且在真正移动的时候,有一种酷酷的感觉。打开HelloWorldScene.cpp文件,在update方法里面,在代码_hero->update(dt);后面添加如下代码:
1
2 3 4 5 6 7 |
CCSize winSize = CCDirector::sharedDirector()->getWinSize();
float scale = (winSize.height * 3 / 4) / _hero->getPosition().y; if (scale > 1) { scale = 1; } _terrain->setScale(scale); |
如果英雄在winSize.height的3/4处的话,拉伸比例为1。如果小于的话,拉伸比例增大。如果大于的话,拉伸比例减小,就有画面缩小的感觉。编译运行,现在飞高就能看到画面的缩放了,如下图所示:
如果不容易达到飞那么高,可以将Hero.cpp文件里的dive方法,增大向下冲量,就比较容易飞高起来。
8.动画和音乐。打开Hero.h文件,添加如下变量:
1
2 |
cocos2d::CCAnimation *_normalAnim;
cocos2d::CCAction *_normalAnimate; |
打开Hero.cpp文件,在构造函数里面,添加如下代码:
1
2 |
_normalAnim =
NULL;
_normalAnimate = NULL; |
在析构函数里面添加如下代码:
1
|
CC_SAFE_RELEASE_NULL(_normalAnim);
|
添加如下方法:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void Hero::nodive()
{ this->runNormalAnimation(); } void Hero::runForceAnimation() { if (_normalAnimate) { this->stopAction(_normalAnimate); } _normalAnimate = NULL; this->setDisplayFrame(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( "seal_downhill.png")); } void Hero::runNormalAnimation() { if (_normalAnimate || !_awake) { return; } _normalAnimate = CCRepeatForever::create(CCAnimate::create(_normalAnim)); this->runAction(_normalAnimate); } |
在initWithWorld方法里面,最后面添加如下代码:
1
2 3 4 5 |
_normalAnim = CCAnimation::create();
_normalAnim->retain(); _normalAnim->addSpriteFrame(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( "seal1.png")); _normalAnim->addSpriteFrame(CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName( "seal2.png")); _normalAnim->setDelayPerUnit( 0.1f); |
这里为海豹的正常移动创建了动画,以及一个运行该动画的方法。俯冲的动画其实只是一个精灵帧,所以添加一个辅助方法来进行设置。打开HelloWorldScene.cpp文件,在onEnter方法里面,最后面添加如下代码:
1
|
CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic(
"TinySeal.mp3",
true);
|
在update方法里面,代码_hero->limitVelocity();上面,添加如下代码:
1
2 3 4 |
else
{ _hero->nodive(); } |
在ccTouchesBegan方法里面,最后面添加如下代码:
1
|
_hero->runForceAnimation();
|
在ccTouchesEnded方法和ccTouchesCancelled方法里面,最后面添加代码:
1
|
_hero->runNormalAnimation();
|
最后,打开Terrain.cpp文件,在draw方法里面,注释掉_world->DrawDebugData();代码。编译运行,最终如下图所示:
参考资料:
1.How To Create A Game Like Tiny Wings with Cocos2D 2.X Part 2 http://www.raywenderlich.com/32958/how-to-create-a-game-like-tiny-wings-with-cocos2d-2-x-part-2
2.(译)如何制作一个类似tiny wings的游戏:第二部分(完) http://www.cnblogs.com/andyque/archive/2011/07/02/2095527.html
非常感谢以上资料,本例子源代码附加资源下载地址:http://download.csdn.net/detail/akof1314/5800479
iOS代码例子:http://vdisk.weibo.com/s/BDn59yfnBVuLf
如文章存在错误之处,欢迎指出,以便改正。