您好,登录后才能下订单哦!
文章原版为英文版,地址链接在文章尾部给出。原文代码版本为object-c版,本文代码版本为C++版。对原文大部分内容进行了翻译,并将对oc版的说明更改为C++版。文章cocos2d-x版本cocos2d-1.0.1-x-0.11.0。
如何用Box2D和cocos2d-x制作弹弓类游戏 第一部分
这是一篇由ios教程团队成员Gustavo Ambrozio上传的博客。一位拥有超过20年软件开发经验,超过3年ios开发经验的软件工程师,CodeCrop软件创始人。
在这个教程系列中我们将会通过使用cocos2d-x和Box2D创建一个很COOL的弹弓类型游戏。
我们将使用Ray的可爱而富有天赋的老婆Vicki创作的弹弓,栗子,狗,猫和愤怒的松鼠素材来创建游戏。(素材我会在上传附件)
在这个教程系列,你将学到:
怎么用旋转关节(rotation joints)
怎么用连接关节(weld joints)
怎么让视角跟随抛射物
怎么根据碰撞检测判断力量来消除敌人
和很多其他的
这个教程系列假设你已经掌握了 Intro to Box2D with Cocos2D Tutorial: Bouncing Balls Tutorial或者已经掌握了相关知识。
教程中还会使用很多制作撞球游戏中的概念。
开始吧
新建HelloWorld项目,清空项目。记得选择需要Box2d支持的cocos2d-x工程。声明一个catapult类。和HelloWorld类除了名字全都一样。
加入些精灵
首先我们先添加项目将用的资源。
现在我们来加入些不会被物理模拟的精灵。默认的CCSprite的锚点是中心,我将锚点改到了左下角为了更容易的放置它们。
在init方法中// add your codes below...下面添加代码:
CCSprite *sprite = CCSprite::spriteWithFile("bg.png"); //背景图
sprite->setAnchorPoint(CCPointZero);
this->addChild(sprite, -1);
CCSprite *sprite = CCSprite::spriteWithFile("catapult_base_2.png"); //投射器底部后面那块
sprite->setAnchorPoint(CCPointZero);
sprite->setPosition(CCPointMake(181.0, FLOOR_HEIGHT));
this->addChild(sprite, 0);
sprite = CCSprite::spriteWithFile("squirrel_1.png"); //左边松鼠
sprite->setAnchorPoint(CCPointZero);
sprite->setPosition(CCPointMake(11.0, FLOOR_HEIGHT));
this->addChild(sprite, 0);
sprite = CCSprite::spriteWithFile("catapult_base_1.png"); //投射器底部前面那块
sprite->setAnchorPoint(CCPointZero);
sprite->setPosition(CCPointMake(181.0, FLOOR_HEIGHT));
this->addChild(sprite, 9);
sprite = CCSprite::spriteWithFile("squirrel_2.png"); //右边松鼠
sprite->setAnchorPoint(CCPointZero);
sprite->setPosition(CCPointMake(240.0, FLOOR_HEIGHT));
this->addChild(sprite, 9);
sprite = CCSprite::spriteWithFile("fg.png"); //带冰的地面
sprite->setAnchorPoint(CCPointZero);
this->addChild(sprite, 10);
你也许注意到了很多使用Y坐标的地方用了宏FLOOR_HEIGHT,但我们并未define它。
#define FLOOR_HEIGHT 62.0f
定义了这个宏之后,如果我们改变了地板高度,我们可以更加简便的放置精灵。
上效果图。
看起来不错!
上面就是非物理模拟的部分。
增加弹弓臂
是时候给世界加些物理属性了,接下来的代码就是加世界边框的模板式的代码了,让我们改变一点来描述我们的世界。
类声明中添加:
private:
b2World* m_world;
b2Body* m_groundBody;
init方法尾部添加:
b2Vec2 gravity;
gravity.Set(0.0f, -10.0f);
bool doSleep = true;
m_world = new b2World(gravity);
m_world->SetAllowSleeping(doSleep);
m_world->SetContinuousPhysics(true);
// Define the ground body.
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0, 0); // bottom-left corner
// Call the body factory which allocates memory for the ground body
// from a pool and creates the ground box shape (also from a pool).
// The body is also added to the world.
m_groundBody = m_world->CreateBody(&groundBodyDef);
默认是世界的尺寸是iphone屏幕尺寸。因为我们场景的宽度是世界宽度的2被。完成这个任务我们只需要让宽度乘以1.5.
另外,由于我们世界的地板并不是在屏幕的底部,所以我们需要编写相应的代码。
边界代码:
b2EdgeShape groundBox;
// bottom
groundBox.Set(b2Vec2(0,FLOOR_HEIGHT/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,FLOOR_HEIGHT/PTM_RATIO));
m_groundBody->CreateFixture(&groundBox, 0);
// top
groundBox.Set(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*2.0f/PTM_RATIO,screenSize.height/PTM_RATIO));
m_groundBody->CreateFixture(&groundBox, 0);
// left
groundBox.Set(b2Vec2(0,screenSize.height/PTM_RATIO), b2Vec2(0,0));
m_groundBody->CreateFixture(&groundBox, 0);
// right
groundBox.Set(b2Vec2(screenSize.width*1.5f/PTM_RATIO,screenSize.height/PTM_RATIO), b2Vec2(screenSize.width*1.5f/PTM_RATIO,0));
m_groundBody->CreateFixture(&groundBox, 0);
说明:box2d某次更新后以前的SetAsEdge函数被删除了,但是可以使用b2EdgeShape类型对象来生成边界,函数名也变为Set。
现在让我们增加弹弓臂,首先增加物体(body)和夹具(fixture)的指针。打开HelloWorld.h把下面的代码加入到类中。
private:
b2Fixture *m_armFixture;
b2Body *m_armBody;
进入到HelloWorld.cpp文件中的init函数的底部:
// Create the catapult's arm
CCSprite *arm = CCSprite::spriteWithFile("catapult_arm.png");
this->addChild(arm, 1);
b2BodyDef armBodyDef;
armBodyDef.type = b2_dynamicBody;
armBodyDef.linearDamping = 1;
armBodyDef.angularDamping = 1;
armBodyDef.position.Set(230.0f/PTM_RATIO, (FLOOR_HEIGHT+91.0f)/PTM_RATIO);
armBodyDef.userData = arm;
m_armBody = m_world->CreateBody(&armBodyDef);
b2PolygonShape armBox;
b2FixtureDef armBoxDef;
armBoxDef.shape = &armBox;
armBoxDef.density = 0.3F;
armBox.SetAsBox(11.0f/PTM_RATIO, 91.0f/PTM_RATIO);
m_armFixture = m_armBody->CreateFixture(&armBoxDef);
你如果看过之前的Box2D教程那么这些代码对你而言应该很熟悉。
我们先读取弹弓臂精灵并把它加入到层中。注意z轴索引。当我们向scene中加入静态精灵时候我们使用Z轴索引。
让我们的弹弓臂位于2块弹弓底部之间看起来不错!
类声明中增加:
void tick(cocos2d::ccTime dt);
cpp文件增加:
void HelloWorld::tick(ccTime dt)
{
int velocityIterations = 8;
int positionIterations = 1;
m_world->Step(dt, velocityIterations, positionIterations);
//Iterate over the bodies in the physics world
for (b2Body* b = m_world->GetBodyList(); b; b = b->GetNext())
{
if (b->GetUserData() != NULL) {
//Synchronize the AtlasSprites position and rotation with the corresponding body
CCSprite* myActor = (CCSprite*)b->GetUserData();
myActor->setPosition( CCPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO) );
myActor->setRotation( -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()) );
}
}
}
在init方法尾部加入:
schedule(schedule_selector(Catapult::tick));
看到没?我们并没有设置精灵的位置,因为tick方法会更正精灵的位置到box2D物体的位置。
接下来我们创建box2d物体作为一个动态物体。userData属性在这里很重要,因为正如我上段提到的,精灵会跟随物体。
另外注意到坐标被设置到在FLOOR_HEIGHT之上。因为我们这里使用的坐标是物体的中心,我们不能用左下角在使用Box2d时候。
接下来就是创建物体物理特性的夹具,一个简单的矩形。
我们设置物体夹具的大小比精灵尺寸小一点,因为精灵尺寸比实际弹弓臂图案尺寸大一点。
在这幅图中,黑色矩形框是精灵尺寸,红色矩形框是夹具尺寸。
运行你会看到机器臂直立着。
旋转关节
我们需要某种约束来限制投射器的转动在一定角度内。借助关节(joints)你可以约束Box2D关联物体运动.
有一种特殊的关节可以完美解决我们的问题——旋转关节(revolute joint)。想象一个钉子将2个物体钉在一个特殊的点,但仍然允许他们转动。
让我们试试吧!回到HelloWorld.h在类中加入属性:
b2RevoluteJoint *m_armJoint;
回到类实现文件在生成发射器臂之后加入下面的代码:
b2RevoluteJointDef armJointDef;
armJointDef.Initialize(m_groundBody, m_armBody, b2Vec2(233.0f/PTM_RATIO, FLOOR_HEIGHT/PTM_RATIO));
armJointDef.enableMotor = true;
armJointDef.enableLimit = true;
armJointDef.motorSpeed = -10; //-1260;
armJointDef.lowerAngle = CC_DEGREES_TO_RADIANS(9);
armJointDef.upperAngle = CC_DEGREES_TO_RADIANS(75);
armJointDef.maxMotorTorque = 700;
m_armJoint = (b2RevoluteJoint*)m_world->CreateJoint(&armJointDef);
当我们创建关节时你不得不修改2个物体和连接点。你可能会想:“我们不应该把投射器臂连接到投射器底部吗》”。在现实世界中,没错。
但是在Box2D中这可不是必要的。你可以这样做但你不得不再为投射器底部创建另一个物体并增加了模拟的复杂性。
由于投射器底部在何时都是静态的并且在Box2d中枢纽物体(hinge body)不必要在其他的任何物体中,我们可以只使用我们已拥有的大地物体(groundBody)。
角度限制外加马达(motor)然我们的投射器更加像真实世界中的投射器。
你也许会注意到我们在关节上设置一个马达激活,通过设置“enableMotor”,“motorSpeed”,和“maxMotorTorque”。
通过设置马达速度为负,可以使投射器臂持续的顺时针转动。
然而,我们还需要通过设置”enableLimit“,”lowerAngle“,”upperAngle“激活关节。这让关节活动范围角度9到75°。这如我们所想的那样模拟投射器运动。
然后我们为了向后拉动投射器将增加另一个关节。当我们释放这个力后马达会让投射器臂向前运动,更像真实的投射器啦!
马达的速度值是每秒弧度值。没错,不是很直观,我知道。我所做的就是不听修改值直到获得了我想的效果。你可以从小的值开始增加知道你获得了期望的速度。最大马达扭矩(maxMotorTorque)是马达可达的最大扭矩。你可以改变这个值来看看物体的反应。那么你会清楚他的作用。
运行app你会看到投射器臂位置现在偏左了:
推动投射器臂吧!
好的,现在是时候移动这个投射器臂啦!为了完成这个任务我们将会使用鼠标关节(mouse joint)。如果你读了雷的弹球游戏教程你就一定已经知道鼠标关节是什么了。
但你没读过,这里是Ray的定义:
“In Box2D, a mouse joint is used to make a body move toward a specified point.”
那就是我们正需要的,所以,然我们先声明一个鼠标关节变量在类定义中:
private:
b2MouseJoint *m_mouseJoint;
public:
virtualvoid ccTouchesBegan(cocos2d::CCSet* touches, cocos2d::CCEvent* event);
virtualvoid ccTouchesMoved(cocos2d::CCSet* touches, cocos2d::CCEvent* event);
virtualvoid ccTouchesEnded(cocos2d::CCSet* touches, cocos2d::CCEvent* event);
接下来在类实现文件增加CCTouchesBegan方法:
void Catapult::ccTouchesBegan(cocos2d::CCSet* touches, cocos2d::CCEvent* event)
{
if (m_mouseJoint != NULL) { return; }
CCTouch *touch = (CCTouch *)touches->anyObject();
CCPoint location = touch->locationInView(touch->view());
location = CCDirector::sharedDirector()->convertToGL(location);
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);
if (locationWorld.x < m_armBody->GetWorldCenter().x + 150.0/PTM_RATIO)
{
b2MouseJointDef md;
md.bodyA = m_groundBody;
md.bodyB = m_armBody;
md.target = locationWorld;
md.maxForce = 2000;
m_mouseJoint = (b2MouseJoint *)m_world->CreateJoint(&md);
}
}
在init方法中调用tick方法代码前:
m_mouseJoint = NULL;
又是引用Ray的话:
"When you set up a mouse joint, you have to give it two bodies. The first isn’t used, but
the convention is to use the ground body. The second is the body you want to move”.
当你创建一个鼠标关节,你不得不给他2个物体,第一个实际上并不会被使用,但为了方便就把ground body赋给它吧,第二个是你想移动的物体。
目标就是我们想要用关节来移动物体。我们首先要将触摸点左边转换成cocos2d-x坐标在转换为Box2D坐标。我们只在触摸在投射器臂物体左边时才会创建关节。50像素的偏移让我们可以触摸投射器臂稍微偏右的位置。
最大力参数将决定应用在投射器臂上跟随目标点移动的最大力。就我们而言,我们不得不确保它足够强壮来抵消被应用与旋转关节的马达的扭矩。
In our case we have to make it strong enough to counteract the torque applied by the motor of the revolute joint.
如果你把这个值设置的太小,那么你将不能拉回投射器臂,因为它的扭矩过大。你可以减小我们的转动关节的最大马达扭矩(maxMotorTorque)或增大最大力(maxForce)参数。
为了确定什么值合适我建议你用下鼠标关节的最大力参数和旋转关节的最大马达扭矩参数。减小maxForce到500并试验,你将看到你不能推动投射器臂。那么减小maxMotorTroque到1000你将看到你又可以推动它了。但让我们先完成这些的实现。。
我们现在不得不实现CCTouchesMoved方法,这样鼠标关节会跟随你的触摸:
void Catapult::ccTouchesMoved(cocos2d::CCSet* touches, cocos2d::CCEvent* event)
{
if (m_mouseJoint == NULL) { return; }
CCTouch *touch = (CCTouch *)touches->anyObject();
CCPoint location = touch->locationInView(touch->view());
location = CCDirector::sharedDirector()->convertToGL(location);
b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO);
m_mouseJoint->SetTarget(locationWorld);
}
这个方法足够简单。它只是把屏幕坐标转换为世界坐标,在将鼠标关节的目标点转换为这个点。
为了完成它我们不得不通过销毁鼠标关节释放投射器臂。让我们在CCTouchesEnded方法中实现它:
void Catapult::ccTouchesEnded(cocos2d::CCSet* touches, cocos2d::CCEvent* event)
{
if (m_mouseJoint != NULL)
{
m_world->DestroyJoint(m_mouseJoint);
m_mouseJoint = NULL;
return;
}
}
很简单!只是销毁了关节清理了变量。试试看程序吧!用你的鼠标拉动投射器臂。当你放开左键时你会看到投射器臂很快的恢复到初始位置。
这真是太快了。正如你记得的,控制它的速度的,就是旋转关节(revolute joint)的马达速度(motorSpeed)和最大马达扭矩(maxMotorTorque)。让我们减小马达速度来看看会发生什么。
回到init方法改小值。一个让我的程序工作的不错的值是-10.改变这个值然后你会看到速度是用来让投射器看起来更真实的参数。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。